Một Bash script backup server chỉ đáng tin khi nó cho biết rõ ba điều: dữ liệu nào đã được sao lưu, bản backup có hoàn tất hay không và người vận hành sẽ nhận cảnh báo bằng cách nào khi thất bại. Việc tạo ra một file có tên backup-2026-09-02.tar.gz chưa chứng minh được file đó có đủ dữ liệu hoặc có thể restore.
Bài này xây dựng một mẫu backup thực tế cho Linux server: dùng Bash để điều phối, tar cho file archive, mysqldump hoặc công cụ dump phù hợp cho database, checksum để kiểm tra file, retention để dọn bản cũ, flock để chống chạy chồng, log có timestamp và alert khi exit status khác 0. Đây là khung tham khảo; bạn phải thay đường dẫn, engine database, remote storage và chính sách secrets trước khi đưa vào production.
Backup chỉ có giá trị vận hành khi quy trình restore đã được kiểm thử và người trực ca biết cách xử lý khi job thất bại.Nguyên tắc thiết kế backup của TechNT.
1. Mô hình backup cần có những lớp nào?
Một backup production nên tách dữ liệu thành các lớp. Database cần logical dump hoặc snapshot phù hợp engine. Uploads và source cần copy hoặc archive giữ đúng permission, owner và symlink. Cấu hình gồm Nginx, systemd unit, cron, environment template và certificate metadata theo chính sách. Cuối cùng là log, manifest và checksum để biết bản nào đã tạo thành công.
| Lớp dữ liệu | Cách sao lưu thường dùng | Điểm cần xác minh |
|---|---|---|
| Database | mysqldump, mariadb-dump, pg_dump hoặc snapshot engine | Dump có exit code 0, file không rỗng, restore thử được |
| Uploads/source | tar hoặc rsync archive mode | Đủ path, permission, owner, symlink và dung lượng |
| Config | tar các thư mục chọn lọc và export crontab/unit | Không làm lộ secret trong log; biết file nào cần restore |
| Backup metadata | manifest, checksum, timestamp và log | Có thể đối chiếu bản backup với thời điểm chạy |
GNU tar mô tả archive là tập hợp file có thể giữ tên, cấu trúc thư mục, owner, group, permission, size và modification time; manual cũng nêu tar được dùng cho backup full và incremental.[1] Với copy đồng bộ sang máy khác, rsync có delta-transfer giúp chỉ gửi phần khác biệt và có thể giữ các thuộc tính file theo option.[2]
2. Nguyên tắc an toàn trước khi viết script
Không đặt password database, token webhook hoặc private key trực tiếp trong script. Dùng file credential có permission chặt, biến môi trường từ secret manager hoặc cơ chế xác thực riêng của công cụ database. Backup trên cùng VPS chỉ bảo vệ khỏi một số lỗi xóa nhầm; nó không bảo vệ khi disk hỏng, tài khoản bị chiếm hoặc provider mất máy.
Đường dẫn backup phải là thư mục riêng, không nằm bên trong thư mục source được archive nếu không muốn script tự sao lưu chính backup của nó. Hãy chặn các biến rỗng, kiểm tra thư mục nguồn tồn tại và dùng đường dẫn tuyệt đối. Trước khi bật retention, chạy ở chế độ list hoặc dry-run để chắc rằng pattern chỉ khớp file backup.
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
BACKUP_ROOT=/srv/backups/example
SOURCE_ROOT=/srv/example
LOG_DIR=/var/log/example-backup
RETENTION_DAYS=14
[[ -n "$BACKUP_ROOT" && "$BACKUP_ROOT" != / ]] || {
printf '%s\n' "BACKUP_ROOT không hợp lệ" >&2
exit 2
}
mkdir -p "$BACKUP_ROOT" "$LOG_DIR"Bash Reference Manual định nghĩa exit status là giá trị command trả về cho caller; shell có thể dùng giá trị đó để kiểm tra hoặc xử lý.[3] Vì vậy script không nên luôn kết thúc bằng echo backup done rồi trả về 0 khi một bước trước đã thất bại. Strict mode cần được dùng có chủ đích và phải hiểu các ngoại lệ như pipeline, command substitution và lệnh kiểm tra dự kiến trả non-zero.
3. Logging: stdout, stderr và timestamp
Log cần trả lời được ai chạy, bắt đầu lúc nào, tạo file nào, bước nào thất bại và tổng thời gian bao lâu. Có thể chuyển stdout/stderr qua tee để vừa hiện trên terminal vừa ghi file; khi chạy bằng cron hoặc systemd, log file nên có retention riêng để không làm đầy disk. Bài cron trên Linux giải thích vì sao môi trường daemon không giống phiên SSH.
timestamp() { date --iso-8601=seconds; }
log() { printf '[%s] %s\n' "$(timestamp)" "$*"; }
RUN_ID=$(date +%Y%m%d-%H%M%S)
LOG_FILE="$LOG_DIR/backup-$RUN_ID.log"
exec > >(tee -a "$LOG_FILE") 2>&1
log "START run_id=$RUN_ID"Nếu dùng exec > >(tee ...), hãy kiểm tra hành vi khi tee hoặc thư mục log lỗi. Với môi trường đơn giản, redirect trực tiếp từ cron cũng đủ, nhưng phải thống nhất nơi đọc log. Không ghi nội dung dump hoặc secret vào log; chỉ ghi path, size, checksum, exit code và thông tin chẩn đoán cần thiết.
4. Trap và cảnh báo khi script thất bại
Trap giúp ghi lại line, command và exit code khi script dừng bất thường. Alert có thể là email local, webhook nội bộ, monitoring hoặc một hệ thống ticket. Không nên coi curl tới webhook là bước duy nhất để biết backup lỗi; alert itself cũng có thể thất bại, nên log phải là nguồn sự thật thứ hai.
ALERT_URL=${ALERT_URL:-}
on_error() {
local exit_code=$?
log "ERROR exit=$exit_code line=${BASH_LINENO[0]} command=${BASH_COMMAND}"
if [[ -n "$ALERT_URL" ]]; then
curl --fail --silent --show-error --max-time 10 \
-X POST -H 'Content-Type: application/json' \
--data "{\"text\":\"Backup failed: $RUN_ID exit=$exit_code\"}" \
"$ALERT_URL" || log "WARN alert delivery failed"
fi
exit "$exit_code"
}
trap on_error ERR
trap 'log "END run_id=$RUN_ID"' EXITTrong đoạn mẫu, alert chỉ gửi thông tin tối thiểu và không đưa đường dẫn chứa secret vào payload. Nếu webhook yêu cầu token, đặt token trong file permission 600 hoặc secret store. Với email, cần kiểm tra MTA local và chính sách relay; đừng giả định lệnh mail luôn tồn tại trên VPS mới.
5. Dump database đúng cách
Database dump phải dùng công cụ đúng engine và tùy chọn phù hợp workload. MariaDB/MySQL thường dùng mariadb-dump hoặc mysqldump; PostgreSQL dùng pg_dump. Với database đang hoạt động, chọn tùy chọn consistency theo engine, transaction, lock và kích thước dữ liệu. Trước production, hãy đo thời gian dump và thử restore trên máy tạm.
DB_DUMP="$STAGING_DIR/database.sql"
# Ví dụ MariaDB/MySQL; dùng credential file thay vì password trên command line
mariadb-dump \
--defaults-extra-file=/root/.config/example/mariadb.cnf \
--single-transaction \
--routines --events --triggers \
example_db > "$DB_DUMP"
[[ -s "$DB_DUMP" ]] || { log "Database dump rỗng"; exit 1; }Tên công cụ và option có thể khác theo distro/engine, nên kiểm tra mariadb-dump --help hoặc tài liệu phiên bản đang chạy. Không đưa password bằng -pPASSWORD vì có thể lộ trong shell history hoặc process listing. File credential phải thuộc user phù hợp, permission chặt và được đưa vào threat model; bản backup chứa database cũng cần mã hóa khi rời máy.
6. Archive file bằng tar hoặc đồng bộ bằng rsync
Tar phù hợp khi cần tạo một artifact có timestamp, dễ checksum và chuyển sang object storage. Rsync phù hợp khi cần đồng bộ lặp lại sang máy đích, giảm dữ liệu truyền và giữ thuộc tính file. Rsync manual mô tả công cụ hỗ trợ local/remote copy, delta-transfer, preserve links/owners/groups/permissions và remote shell như SSH.[2]
# Danh sách nguồn nên được kiểm tra trước
SOURCE_PATHS=(
/srv/example/current
/srv/example/shared/uploads
/etc/nginx
/etc/systemd/system
)
for path in "${SOURCE_PATHS[@]}"; do
[[ -e "$path" ]] || { log "Thiếu source: $path"; exit 1; }
done
tar --create --gzip --file="$ARCHIVE" \
--directory=/ \
srv/example/current \
srv/example/shared/uploads \
etc/nginx \
etc/systemd/systemKhi dùng tar với đường dẫn tuyệt đối, chọn cách biểu diễn nhất quán để lúc restore không vô tình giải nén sai nơi. Sau khi tạo archive, chạy tar --test-label không thay thế được kiểm thử nội dung; hãy dùng tar -tzf để kiểm tra danh sách member, size file và một vài path bắt buộc. Không archive thư mục backup vào chính nó.
7. Mẫu script hoàn chỉnh có lock, checksum và retention
Đoạn dưới đây là khung có thể dùng để triển khai sau khi đã thay biến theo hệ thống. Nó tạo thư mục tạm, dump database, archive file và config, ghi manifest, tạo checksum, đổi tên artifact sau khi hoàn tất rồi mới dọn bản cũ. Việc dùng file tạm tránh để monitoring hiểu nhầm một archive đang ghi dở là bản backup hoàn chỉnh.
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
BACKUP_ROOT=/srv/backups/example
SOURCE_ROOT=/srv/example
RETENTION_DAYS=14
RUN_ID=$(date +%Y%m%d-%H%M%S)
WORK_DIR=$(mktemp -d "$BACKUP_ROOT/.work-$RUN_ID.XXXXXX")
FINAL_DIR="$BACKUP_ROOT/$RUN_ID"
LOCK_FILE="$BACKUP_ROOT/backup.lock"
cleanup() { rm -rf -- "$WORK_DIR"; }
on_error() { printf '[%s] ERROR line=%s cmd=%s\n' "$(date -Is)" "${BASH_LINENO[0]}" "$BASH_COMMAND" >&2; }
trap on_error ERR
trap cleanup EXIT
exec 9>"$LOCK_FILE"
flock -n 9 || { echo "Một backup khác đang chạy" >&2; exit 3; }
mkdir -p "$WORK_DIR/files" "$FINAL_DIR"
mariadb-dump --defaults-extra-file=/root/.config/example/mariadb.cnf \
--single-transaction --routines --events --triggers example_db \
> "$WORK_DIR/database.sql"
[[ -s "$WORK_DIR/database.sql" ]]
tar -czf "$WORK_DIR/files/site.tgz" -C / \
srv/example/current srv/example/shared/uploads etc/nginx
tar -czf "$WORK_DIR/files/config.tgz" -C / \
etc/systemd/system
cp -- "$WORK_DIR/database.sql" "$FINAL_DIR/"
cp -- "$WORK_DIR/files/"*.tgz "$FINAL_DIR/"
( cd "$FINAL_DIR" && sha256sum ./* > SHA256SUMS )
( cd "$FINAL_DIR" && tar -tzf site.tgz >/dev/null && sha256sum --check SHA256SUMS )
mv -- "$FINAL_DIR" "$BACKUP_ROOT/complete-$RUN_ID"
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -name 'complete-*' \
-mtime +"$RETENTION_DAYS" -print -exec rm -rf -- {} +Mẫu này cố ý không chứa logic upload remote hoặc alert hoàn chỉnh để tránh tạo cảm giác “copy là chạy production”. Trước khi dùng, hãy test từng bước với database staging, xác định owner cần restore, thêm log, kiểm tra dung lượng và đưa retention vào change review. Nếu backup nằm trên filesystem khác, hãy dùng rsync qua SSH với key giới hạn quyền hoặc công cụ object storage có encryption.
8. Retention: xóa bản cũ như thế nào cho an toàn?
Retention không chỉ là find -mtime +7 -delete. Hãy chỉ xóa trong thư mục backup riêng, khớp đúng prefix artifact hoàn chỉnh và chỉ xóa sau khi bản mới đã checksum/list thành công. Nếu retention theo số bản thay vì số ngày, sort theo timestamp và giữ tối thiểu số bản đáp ứng RPO. Với backup remote, cần xem lifecycle policy của storage thay vì xóa local rồi tưởng remote vẫn còn.
# Xem trước những gì sẽ bị xóa
find /srv/backups/example \
-mindepth 1 -maxdepth 1 -type d -name 'complete-*' \
-mtime +14 -print
# Chỉ bật xóa sau khi đã kiểm tra pattern và dry-run
find /srv/backups/example \
-mindepth 1 -maxdepth 1 -type d -name 'complete-*' \
-mtime +14 -print -exec rm -rf -- {} +Đừng dùng biến rỗng trong rm -rf "$BACKUP_ROOT"/* nếu script có thể chạy khi config lỗi. Chặn root path, kiểm tra prefix và log danh sách đã xóa. Nên giữ ít nhất một bản backup ngoài cùng thời điểm với bản local để giảm rủi ro thao tác nhầm.
9. Lập lịch bằng cron hoặc systemd timer
Cron phù hợp khi script có lịch đơn giản; systemd timer phù hợp khi muốn service unit rõ ràng, journal, dependency và trạng thái chạy. Bài Cron trên Linux đã trình bày PATH, user, timezone và lock; hãy áp dụng các nguyên tắc đó thay vì dán một dòng lịch dài.
# Cron: chạy 02:00, gộp stdout/stderr vào log
0 2 * * * /usr/local/sbin/example-backup.sh >> /var/log/example-backup-cron.log 2>&1# /etc/systemd/system/example-backup.service
[Unit]
Description=Example server backup
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/example-backup.sh
# /etc/systemd/system/example-backup.timer
[Unit]
Description=Run example backup daily
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.targetsystemd timer dùng timer unit để kích hoạt service unit; nếu không chỉ định Unit=, service cùng tên sẽ được dùng mặc định.[4] OnCalendar= là lịch theo wall-clock và Persistent=true có thể giúp xử lý lịch bị bỏ lỡ theo semantics của systemd. Sau khi cài, chạy systemctl daemon-reload, enable timer, xem systemctl list-timers và test service bằng tay.
10. Kiểm thử backup và restore: tiêu chuẩn không thể bỏ qua
Backup thành công phải được xác nhận bằng restore. Tối thiểu, giải nén archive vào thư mục tạm, kiểm tra manifest/checksum, restore một database vào instance riêng, khởi động ứng dụng staging và gọi endpoint read-only. Ghi thời gian dump, thời gian restore, dung lượng và lỗi gặp phải để tính RTO thực tế.
# Kiểm tra archive và checksum
sha256sum --check /srv/backups/example/complete-*/SHA256SUMS
tar -tzf /srv/backups/example/complete-*/site.tgz | sed -n '1,40p'
# Restore database vào DB tạm, không overwrite production
mariadb --defaults-extra-file=/root/.config/example/restore.cnf restore_test \
< /srv/backups/example/complete-*/database.sqlKhông thử restore trực tiếp lên database production nếu chưa có snapshot hoặc kế hoạch rollback. Nếu backup được mã hóa, hãy kiểm tra key recovery độc lập; nếu key nằm duy nhất trên server đã hỏng thì archive còn tồn tại nhưng không có giá trị. Hãy ghi runbook restore ở nơi khác với server được backup.
11. Monitoring: log “có chạy” chưa đủ
Monitoring cần phân biệt job bắt đầu, job hoàn tất, job thất bại, backup quá cũ và dung lượng còn lại. Có thể tạo một file status chỉ cập nhật sau khi checksum và test archive pass, sau đó monitoring kiểm tra tuổi file. Đừng chỉ kiểm tra process cron vì process có thể exit 0 trước khi một upload remote hoàn tất nếu script không chờ đúng.
- Cảnh báo ngay khi exit code khác 0 hoặc alert delivery thất bại.
- Cảnh báo khi không có backup complete mới trong khoảng thời gian kỳ vọng.
- Cảnh báo disk/inode gần đầy ở cả source và destination.
- Định kỳ kiểm tra checksum và thực hiện restore mẫu.
- Theo dõi kích thước backup bất thường; file nhỏ đột ngột thường là tín hiệu lỗi.
12. Các lỗi thường gặp khi viết Bash backup
Lỗi phổ biến nhất là dùng đường dẫn tương đối, bỏ qua exit status trong pipeline, ghi password trên command line, xóa retention trước khi verify bản mới, backup cùng disk với source, chạy chồng nhiều phiên và không test restore. Một lỗi khác là nén database bằng cách copy file dữ liệu đang mở thay vì dùng công cụ dump/backup nhất quán của engine.
Strict mode cũng không phải bùa hộ mệnh. set -e có những ngữ cảnh đặc biệt trong điều kiện, pipeline và command substitution; set -u sẽ dừng khi biến chưa khai báo; pipefail giúp pipeline phản ánh lỗi ở command trước. Hãy viết test nhỏ cho các nhánh lỗi thay vì chỉ chạy happy path.
13. Checklist production cho Bash backup server
- Liệt kê database, uploads, source, config, secrets và metadata cần sao lưu.
- Dùng credential an toàn; không hard-code password hoặc token vào script/log.
- Kiểm tra source path, destination path, disk space, inode và permission trước khi chạy.
- Dùng lock chống chạy chồng và thư mục tạm cho artifact đang ghi.
- Ghi log có timestamp, exit code, run ID, size và checksum.
- Chỉ publish artifact sau khi dump, archive và verify hoàn tất.
- Áp dụng retention có dry-run, prefix chặt và giữ bản ngoài máy.
- Gửi alert khi lỗi, nhưng vẫn giữ log làm bằng chứng điều tra.
- Test restore định kỳ và đo RTO/RPO thay vì chỉ kiểm tra file tồn tại.
- Ghi runbook restore, key recovery và cách disable lịch backup.
14. Kết luận
Bash là lớp điều phối linh hoạt cho backup server, nhưng độ tin cậy đến từ thiết kế chứ không đến từ vài dòng tar czf. Một script production cần có boundary rõ, exit status, log, lock, retention, checksum, cảnh báo và quy trình restore. Khi chạy qua cron hoặc systemd timer, hãy kiểm tra đúng user, PATH, timezone và journal.
Hãy bắt đầu bằng một backup nhỏ, restore được trong môi trường tạm, sau đó mới thêm remote storage, mã hóa và lịch tự động. Backup chỉ hoàn thành khi bạn có thể trả lời được: bản nào mới nhất, dữ liệu nào nằm trong đó, restore mất bao lâu và ai sẽ xử lý khi cảnh báo bật lên.