Docker và Application Ops

Dữ liệu persistent của Docker container: Backup thế nào cho đúng và lưu ở đâu?

Cách sao lưu database và file upload trong Docker, đồng bộ recovery point, tránh nén sống dữ liệu và chuyển bản backup ra storage độc lập.

Backup dữ liệu persistent container
Mục lục

Rất nhiều người khi mới đưa website chạy Docker Compose lên máy chủ thường có một ngộ nhận tai hại: "Tôi đã khai báo named volume cho MySQL và thư mục upload rồi, dữ liệu không mất đi khi container khởi động lại, tức là an toàn rồi."

Sự thật là: Dữ liệu bền vững (Persistence) không phải là Sao lưu (Backup).

Named volume hay bind mount chỉ có một nhiệm vụ duy nhất: giúp dữ liệu không bị xóa mất khi bạn chạy lệnh nâng cấp hoặc thay thế container (docker compose up -d). Nhưng toàn bộ dữ liệu đó vẫn đang nằm trực tiếp trên ổ cứng của chính máy chủ VPS bạn đang chạy:

  • Nếu ổ cứng VPS bị hỏng vật lý hoặc nhà cung cấp cloud gặp sự cố phần cứng, dữ liệu mất sạch.
  • Nếu máy chủ bị mã độc tống tiền (ransomware) hoặc bị lộ quyền root, toàn bộ volume sẽ bị mã hóa hoặc xóa sổ.
  • Nếu ai đó trong lúc thao tác bảo trì gõ nhầm lệnh hủy diệt docker compose down -v hoặc docker volume prune -a, toàn bộ volume sẽ bốc hơi trong một nốt nhạc mà không có cách nào khôi phục.

Vì vậy, dữ liệu cần phải được trích xuất thành các bản sao lưu độc lập và chuyển ra khỏi chiếc VPS đó.

Sai lầm chết người: Dùng lệnh tar nén sống thư mục database

Một sai lầm rất phổ biến khác là viết cron job định kỳ chạy lệnh tar để nén nguyên thư mục /var/lib/mysql hay /var/lib/postgresql/data của database đang chạy.

Nếu database là một website đang hoạt động, tiến trình database liên tục ghi dữ liệu vào bộ đệm (RAM buffer), ghi file nhật ký giao dịch (WAL/redo log) và khóa các khối dữ liệu. Khi bạn chạy lệnh tar, nó sẽ sao chép các file ở những thời điểm lệch nhau vài giây: file này đã ghi xong nhưng file kia mới ghi một nửa.

Kết quả là bạn tạo ra một bản nén có dung lượng bình thường, nhưng bên trong là dữ liệu bị hỏng (corrupted). Đến ngày sự cố xảy ra, bạn lôi bản nén đó ra giải nén vào container mới thì database sẽ báo lỗi crash ngay lúc boot và từ chối khởi động.

Với database chạy container, quy tắc bất di bất dịch là: Phải dùng công cụ sao lưu chuyên dụng của chính database đó (như mysqldump cho MySQL/MariaDB, pg_dump cho PostgreSQL). Các công cụ này đảm bảo toàn bộ dữ liệu được xuất ra ở một trạng thái giao dịch nhất quán (consistent state) mà không làm ngắt quãng website.

Cách trích xuất dữ liệu chuẩn cho Database và File upload

Trong một stack Docker Compose điển hình, dữ liệu quan trọng nhất thường chia làm 2 phần riêng biệt:

1. Dữ liệu Database (Xuất qua lệnh dump chuyên dụng)

Thay vì đụng vào các file bên trong volume, bạn dùng lệnh docker compose exec để gọi công cụ dump ngay từ bên trong container database:

  • Với MySQL / MariaDB:
bash
  docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" \
    --single-transaction --quick app_db | gzip > /srv/backups/db-$(date +%F).sql.gz

(Cờ -T giúp tắt phân bổ TTY để xuất dữ liệu sạch ra pipe; --single-transaction đảm bảo dữ liệu nhất quán mà không làm khóa các bảng InnoDB trong lúc website đang chạy).

  • Với PostgreSQL:
bash
  docker compose exec -T db pg_dump -U "$POSTGRES_USER" -Fc app_db > /srv/backups/db-$(date +%F).dump

(Định dạng custom -Fc của Postgres vừa tự động nén, vừa cho phép bạn khôi phục song song cực nhanh khi cần).

2. File upload của người dùng (Ảnh, tài liệu, video)

File tĩnh không có tính chất giao dịch phức tạp như database, nên bạn hoàn toàn có thể dùng lệnh nén file thông thường.

Nếu bạn dùng bind mount (ví dụ thư mục mount thẳng vào ./uploads trên máy chủ), bạn chỉ cần nén trực tiếp thư mục đó trên host:

bash
tar -czf /srv/backups/uploads-$(date +%F).tar.gz -C /srv/example/uploads .

Nếu bạn dùng named volume của Docker, bạn có thể chạy một container phụ siêu nhẹ (như alpine) để mount volume ở chế độ chỉ đọc (:ro) rồi nén ra ngoài:

bash
docker run --rm \
  -v example_app_uploads:/data:ro \
  -v /srv/backups:/backup \
  alpine tar -czf /backup/uploads-$(date +%F).tar.gz -C /data .

Gắn cờ :ro (read-only) đảm bảo tiến trình backup tuyệt đối không vô tình ghi đè hay làm thay đổi bất kỳ file nào của người dùng.

Cái bẫy "lệch pha" giữa Database và File upload

Một sai lầm rất dễ gặp là lên lịch cron job tách rời: dump database lúc 02:00 sáng, rồi đến 03:30 sáng mới chạy nén file upload.

Trong khoảng cách hơn 1 tiếng đồng hồ đó, khách hàng vẫn đang hoạt động trên website: người thì tải ảnh đại diện mới, người thì gửi phiếu yêu cầu có đính kèm file, người thì xóa ảnh cũ.

Hậu quả xuất hiện khi bạn phải khôi phục lại hệ thống:

  • Database có bản ghi trỏ đến file ảnh invoice-102.pdf, nhưng trong bản nén file (được chụp ở mốc khác) file đó chưa tồn tại hoặc đã bị xóa. Website sẽ hiển thị lỗi ảnh hỏng (404 Not Found) khắp nơi.
  • Ngược lại, có những file upload tồn tại trong ổ đĩa nhưng database hoàn toàn không có bản ghi nào tương ứng (dữ liệu mồ côi).

Cách giải quyết thực tế: Luôn gom tiến trình backup database và file upload vào cùng một kịch bản (script) duy nhất chạy vào lúc nửa đêm (thời điểm ít người truy cập nhất). Cả hai file backup phải được đánh cùng một nhãn thời gian (timestamp chung, ví dụ backup-2026-10-12-0200). Khi cần phục hồi, bạn luôn khôi phục cả cặp này cùng nhau.

Thư mục /srv/backups trên máy chủ chỉ là trạm trung chuyển

Rất nhiều script backup sau khi tạo xong file .sql.gz và .tar.gz thì lưu ngay vào thư mục /srv/backups trên cùng máy chủ và dừng lại ở đó.

Thư mục đó thực chất chỉ là trạm trung chuyển tạm thời (staging). Nếu toàn bộ file backup nằm chung ổ cứng với Docker:

  • Khi ổ đĩa VPS bị đầy (100% disk), website sẽ sập.
  • Khi nhà cung cấp VPS gặp sự cố cháy trung tâm dữ liệu hoặc lỗi cụm lưu trữ, cả website đang chạy lẫn các file backup sẽ biến mất cùng lúc.

Quy trình chuẩn là ngay sau khi nén xong, bạn phải đẩy ngay các file backup này ra một dịch vụ lưu trữ độc lập bên ngoài, phổ biến và tiết kiệm nhất hiện nay là Object Storage (như Cloudflare R2, AWS S3, Wasabi hay Backblaze B2) thông qua công cụ dòng lệnh như rclone:

bash
# Đẩy bản backup lên Cloudflare R2 / AWS S3
rclone copy /srv/backups/ remote-bucket:my-backups/$(date +%F)/

# Sau khi đẩy thành công, xóa file staging trên VPS để giải phóng ổ cứng
rm -rf /srv/backups/*

Để tối ưu an toàn, hãy bật tính năng Object Lock (WORM - Write Once, Read Many) hoặc Bucket Versioning trên tài khoản lưu trữ này. Ngay cả khi tin tặc chiếm được tài khoản root của VPS và cố tình gửi lệnh xóa dữ liệu trên bucket, dịch vụ lưu trữ vẫn bảo vệ không cho phép xóa các bản backup trong vòng 30 ngày.

3 nguyên tắc vàng để dữ liệu container luôn an toàn

Để không bao giờ phải rơi vào cảnh mất trắng dữ liệu, hãy ghi nhớ 3 nguyên tắc sau:

  1. Tuân thủ quy tắc 3-2-1: Luôn có ít nhất 3 bản sao dữ liệu (1 bản đang chạy trên VPS, 1 bản sao lưu trên Object Storage, và 1 bản lưu trữ lạnh ngoại mạng hoặc ở một nhà cung cấp khác). Chi tiết mô hình này được trình bày tại bài Quy tắc backup 3-2-1 áp dụng cho một VPS nhỏ ra sao?.
  2. Cảnh giác tối đa với các lệnh dọn dẹp: Không bao giờ gõ docker compose down -v trên production nếu bạn không muốn xóa sạch database. Khi dọn dẹp các volume cũ bằng docker volume prune, hãy kiểm tra kỹ danh sách volume đang có để không xóa nhầm dữ liệu đang chờ deploy. Xem thêm quy trình bảo toàn dữ liệu tại Nâng cấp Docker image an toàn và Rollback Docker Compose.
  3. Thử khôi phục (Restore test) định kỳ: Một bản backup chưa từng được restore thử chỉ là một "niềm hy vọng", chưa phải một bản sao lưu thực thụ. Định kỳ mỗi tháng một lần, hãy tải cặp file backup về một máy ảo test hoặc VPS phụ để dựng lại thử theo Checklist thử restore website trên môi trường tách biệt. Chỉ khi website test khởi động mượt mà và đọc đủ dữ liệu, bạn mới có thể kê cao gối ngủ yên.

Khi chuẩn bị đưa toàn bộ ứng dụng lên máy chủ, hãy tham khảo Checklist đưa ứng dụng Docker Compose lên production để bao quát toàn bộ các bước thiết lập volume, cấu hình và giám sát.

Nguồn tham khảo

Chia sẻ bài viết hữu ích

Gửi bài viết này cho đồng nghiệp