2 giờ sáng, database PostgreSQL mất sạch data — bài học đắt giá về volumes
Hôm đó mình deploy lại container PostgreSQL trên production server, chạy docker rm -f postgres rồi tạo lại. Mở app lên — trắng tinh. Toàn bộ dữ liệu 3 tháng biến mất.
Lý do đơn giản đến đau: mình đang lưu data bên trong container, không phải bên ngoài. Container bị xóa — data đi theo. Từ đó mình không bao giờ deploy database mà không kiểm tra volume trước.
Tại sao data trong container lại nguy hiểm
Container Docker có một lớp filesystem gọi là writable layer — mỗi thứ bạn ghi vào trong container (database file, file upload, log…) đều nằm ở đó. Vấn đề: lớp này gắn liền với vòng đời của container. Container chết — data chết theo.
docker stop→ data còn đó (container chỉ dừng)docker rm→ data mất luôn- Deploy version mới (rebuild image, tạo lại container) → data mất
- Server crash, Docker daemon restart → container ephemeral state có thể bị lost
Docker có 3 cách lưu data ra ngoài container: volumes, bind mounts, và tmpfs. Cho production, volumes là lựa chọn chuẩn nhất — Docker tự quản lý, không phụ thuộc cấu trúc thư mục của host.
Khái niệm cơ bản trước khi bắt đầu
Volumes là built-in của Docker, không cần cài thêm gì. Nhưng có 2 loại và sự khác biệt quan trọng hơn bạn nghĩ:
Named volumes vs Anonymous volumes
Anonymous volume: Docker tự sinh tên ngẫu nhiên kiểu b3c2e1f9a8d7..., rất khó track, hay bị quên và âm thầm chiếm disk.
# Anonymous volume — KHÔNG dùng cho production
docker run -v /var/lib/postgresql/data postgres
Named volume: bạn đặt tên có ý nghĩa, dễ quản lý, tồn tại độc lập với container.
# Tạo named volume
docker volume create postgres_data
# Kiểm tra
docker volume ls
docker volume inspect postgres_data
Output của inspect sẽ cho bạn thấy data thực tế nằm ở đâu trên host — thường là /var/lib/docker/volumes/postgres_data/_data.
Cấu hình thực tế từ production
1. PostgreSQL với named volume
# Chạy PostgreSQL với named volume
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=myapp \
-v postgres_data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
# Test: tạo data
docker exec -it postgres psql -U postgres -d myapp -c \
"CREATE TABLE test (id serial, name text); INSERT INTO test (name) VALUES ('hello');"
# Xóa container
docker rm -f postgres
# Tạo lại — data vẫn còn
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=myapp \
-v postgres_data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
docker exec -it postgres psql -U postgres -d myapp -c "SELECT * FROM test;"
# Kết quả: dữ liệu vẫn ở đó
2. Volumes trong Docker Compose
Stack production của mình gồm app server + PostgreSQL + Redis + volume cho user uploads. Đây là cấu hình Compose sau khi đã refine qua vài lần incident thực tế:
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
command: redis-server --appendonly yes
restart: unless-stopped
app:
image: myapp:latest
volumes:
- uploads:/app/public/uploads
depends_on:
- postgres
- redis
volumes:
postgres_data: # Docker tự tạo nếu chưa có
redis_data:
uploads:
Chú ý block volumes: ở cuối file — đây là nơi khai báo tất cả named volumes. Thiếu block này mà vẫn dùng volume trong service, Compose báo lỗi ngay.
3. Bind mounts — chỉ dùng cho development
Bind mounts map trực tiếp thư mục host vào container. Hữu ích cho hot reload khi dev, hoặc khi cần đọc config file từ host:
# Bind mount: map thư mục hiện tại vào /app
docker run -v $(pwd):/app node:20 npm run dev
# Trong Compose — development config
services:
app:
image: myapp:latest
volumes:
- ./src:/app/src # bind mount cho dev
- uploads:/app/uploads # named volume cho data
Trên production, bind mount cho database hay user data gây đau đầu không cần thiết — permission phức tạp, path trên host thay đổi là container lỗi ngay.
4. Volume drivers — lưu trữ ngoài host
Chạy multi-server hoặc cần backup tự động lên cloud? Volume drivers cho phép lưu data lên NFS, S3, hay cloud storage. Ví dụ với NFS:
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=192.168.1.100,rw \
--opt device=:/srv/nfs/mydata \
nfs_data
Team nhỏ, single VPS thì named volume local là đủ. Chỉ cần backup đúng thư mục /var/lib/docker/volumes/ là xong.
Kiểm tra & Monitoring
Xem danh sách và dung lượng volumes
# Liệt kê tất cả volumes
docker volume ls
# Xem chi tiết một volume
docker volume inspect postgres_data
# Xem dung lượng (cần quyền root)
du -sh /var/lib/docker/volumes/postgres_data/_data
# Hoặc dùng docker system df để xem tổng quan
docker system df -v
Tìm container đang dùng volume nào
# Xem volumes của container đang chạy
docker inspect postgres --format='{{json .Mounts}}' | python3 -m json.tool
Backup và restore volume
Sau sự cố 2 giờ sáng, đây là script mình bổ sung vào cronjob hàng ngày — đơn giản nhưng đã cứu mình ít nhất 2 lần:
# Backup volume ra file tar.gz
docker run --rm \
-v postgres_data:/data \
-v $(pwd)/backups:/backup \
alpine tar czf /backup/postgres_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
# Restore từ backup
docker run --rm \
-v postgres_data:/data \
-v $(pwd)/backups:/backup \
alpine tar xzf /backup/postgres_20240101_020000.tar.gz -C /data
Dọn dẹp volumes không dùng
# Xem danh sách volumes không có container nào dùng
docker volume ls -f dangling=true
# Xóa tất cả dangling volumes — CẨN THẬN, không thể undo
docker volume prune
# Xóa một volume cụ thể (phải stop container trước)
docker volume rm ten_volume
Lưu ý: docker system prune KHÔNG xóa volumes trừ khi bạn thêm flag --volumes. Đây là thiết kế có chủ ý của Docker để bảo vệ data — đừng thêm flag đó vào script dọn dẹp tự động trên production.
Monitoring disk usage
PostgreSQL WAL logs, Redis RDB snapshots — những thứ này ăn disk âm thầm nếu không để ý. Mình thêm check này vào monitoring script, cảnh báo khi vượt 80%:
#!/bin/bash
# Cảnh báo nếu volumes dùng quá 80% disk
DISK_USAGE=$(df /var/lib/docker/volumes | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK_USAGE" -gt 80 ]; then
echo "ALERT: Docker volumes disk usage at ${DISK_USAGE}%"
docker system df -v
fi
Checklist production
- Tất cả database (PostgreSQL, MySQL, MongoDB, Redis) đều phải dùng named volume
- User uploads, generated files — named volume
- Backup cronjob chạy ít nhất 1 lần/ngày cho mỗi volume quan trọng
- Test restore ít nhất 1 lần/tháng — backup không test là backup không tồn tại
- Không dùng
docker system prune --volumestrên production trừ khi đã backup - Document tên volume và container nào dùng — 3 tháng sau bạn sẽ quên hết
Từ sau bài học 2 giờ sáng đó, mỗi lần setup stack mới mình đều chạy docker inspect để chắc data đang vào volume, không phải writable layer. Ba mươi giây đó đã cứu mình khỏi ít nhất vài buổi debug lúc nửa đêm rồi.

