Docker Deev Dive #5: Volume — Persist data bên ngoài container

8 phút đọc

Overview

Ở bài 4 bạn đã thấy: xóa container → mất hết dữ liệu bên trong. Đó là vì writable layer của container bị xóa cùng container.

Volume là cơ chế để persist (lưu trữ lâu dài) data bên ngoài container lifecycle. Data được lưu trên host filesystem, tách biệt hoàn toàn khỏi container — container chết, data vẫn sống.

💡 Mental model: Container giống phòng khách sạn (check-out là mất hết). Volume giống két sắt ngoài phòng — bạn check-out rồi quay lại vẫn lấy được đồ.


Key Concepts

Vấn đề cốt lõi

Docker container sử dụng Union Filesystem (bài 2). Mỗi container có một writable layer ở trên cùng:

  • Ghi file → ghi vào writable layer (Copy-on-Write).
  • Xóa container → xóa writable layer → mất toàn bộ data.

Với database (MySQL, PostgreSQL), log files, upload files — mất data là thảm họa. Volume giải quyết chính xác vấn đề này.

3 cách mount data trong Docker

Loại Cú pháp ví dụ Ai quản lý Use case chính
Named Volume mysql_data:/var/lib/mysql Docker Engine Database, persistent data
Bind Mount ./src:/var/www/html Developer (host path) Source code dev, config files
tmpfs Mount --tmpfs /tmp RAM (không ghi disk) Sensitive data tạm thời, cache

⚠️ Trong thực tế Laravel dev, bạn sẽ dùng Named Volume cho DB và Bind Mount cho source code — 90% trường hợp chỉ cần 2 loại này.


How It Works

1. Named Volume

Cơ chế: Docker tạo một directory riêng trên host (thường tại /var/lib/docker/volumes/<volume_name>/_data), sau đó mount vào container path.

# Tạo volume
docker volume create mysql_data

# Chạy MySQL container với named volume
docker run -d \
  --name mysql_db \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=laravel \
  -v mysql_data:/var/lib/mysql \
  mysql:8.0

Data flow:

Container: /var/lib/mysql  ←──mount──→  Host: /var/lib/docker/volumes/mysql_data/_data
  • Container ghi data vào /var/lib/mysql → thực tế ghi vào host directory.
  • Xóa container → volume vẫn tồn tại trên host.
  • Tạo container mới mount cùng volume → data phục hồi nguyên vẹn.

Quản lý volume:

# Liệt kê tất cả volumes
docker volume ls

# Xem chi tiết volume
docker volume inspect mysql_data

# Xóa volume (chỉ khi không container nào đang dùng)
docker volume rm mysql_data

# Dọn tất cả volume không ai dùng
docker volume prune

2. Bind Mount

Cơ chế: Map trực tiếp một folder/file trên host vào container. Không qua Docker volume manager — bạn kiểm soát hoàn toàn path.

# Bind mount source code Laravel vào container
docker run -d \
  --name php_app \
  -v $(pwd)/src:/var/www/html \
  php:8.2-fpm-alpine

Data flow:

Host: ~/projects/my-app/src  ←──bind──→  Container: /var/www/html
  • Sửa code trên host (VSCode) → container thấy ngay (real-time sync).
  • Container ghi file (ví dụ storage/logs) → host cũng thấy.
  • Hai chiều (bidirectional) — cả host và container đều đọc/ghi được.

💡 Đây là lý do khi dev Laravel với Docker, bạn sửa code trên máy mà container tự nhận thay đổi — nhờ bind mount.

3. So sánh Named Volume vs Bind Mount

Tiêu chí Named Volume Bind Mount
Path trên host Docker tự quản lý (/var/lib/docker/volumes/...) Bạn chỉ định cụ thể (./src, /home/user/data)
Portability Cao — không phụ thuộc host path Thấp — phụ thuộc cấu trúc folder host
Performance (macOS/Windows) Tốt hơn (Docker optimized) Chậm hơn đáng kể (do file sharing layer)
Backup/Migrate Dùng docker volume commands Copy thủ công trên host
Use case Database, persistent app data Source code, config files khi dev
Security Docker quản lý permissions Có thể gặp permission issues (UID/GID mismatch)

Cú pháp -v vs --mount

Docker có 2 cú pháp mount, cả 2 đều hoạt động giống nhau:

# Cú pháp -v (ngắn gọn, phổ biến)
docker run -v mysql_data:/var/lib/mysql mysql:8.0
docker run -v $(pwd)/src:/var/www/html php:8.2-fpm

# Cú pháp --mount (rõ ràng hơn, recommended cho production)
docker run --mount type=volume,source=mysql_data,target=/var/lib/mysql mysql:8.0
docker run --mount type=bind,source=$(pwd)/src,target=/var/www/html php:8.2-fpm

Khác biệt quan trọng: Với -v, nếu host path chưa tồn tại, Docker tự tạo directory. Với --mount, Docker sẽ báo lỗi — an toàn hơn.


Áp dụng cho Laravel Stack

Đây là cách volume thường được setup trong một Laravel Docker project:

# docker-compose.yml (preview — bạn sẽ học Compose ở tuần 3)
services:
  app:
    image: php:8.2-fpm-alpine
    volumes:
      - ./src:/var/www/html        # Bind mount: source code
      - vendor_data:/var/www/html/vendor  # Named volume: vendor (optional, speed)

  mysql:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql   # Named volume: database files
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: laravel

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data            # Named volume: Redis persistence

volumes:
  mysql_data:     # Docker tạo & quản lý
  redis_data:
  vendor_data:

Giải thích:

  • ./src:/var/www/html → Bind mount source code để dev real-time.
  • mysql_data:/var/lib/mysql → Named volume cho MySQL, data sống sót qua container restart/recreate.
  • redis_data:/data → Named volume cho Redis persistence (AOF/RDB).

Trade-offs & Lưu ý

✅ Khi nào dùng Named Volume

  • Database (MySQL, PostgreSQL, Redis).
  • Upload files cần persist.
  • Bất kỳ data nào cần sống sót khi container bị xóa.

✅ Khi nào dùng Bind Mount

  • Source code trong development (real-time edit).
  • Config files (nginx.conf, php.ini) cần custom.
  • Log files cần xem trên host.

⚠️ Permission Issues (phổ biến)

Khi bind mount, container process (thường chạy user www-data với UID 33) có thể không có quyền ghi vào folder host:

# Lỗi thường gặp
# Permission denied: /var/www/html/storage/logs/laravel.log

# Fix: đảm bảo UID match
docker run -v $(pwd)/src:/var/www/html \
  --user $(id -u):$(id -g) \
  php:8.2-fpm-alpine

# Hoặc trong Dockerfile
RUN chown -R www-data:www-data /var/www/html/storage

⚠️ Performance trên macOS/Windows

Bind mount trên macOS/Windows chậm hơn Linux do file sharing layer (osxfs/grpcfuse). Workaround:

  • Dùng consistency: cached hoặc delegated (Docker Compose).
  • Dùng named volume cho vendor/node_modules/ (nặng I/O, ít sửa).
  • Trên Docker Desktop mới: bật VirtioFS trong Settings → General.

Thực hành

Bài 1: Chứng minh Named Volume persist data

# 1. Tạo MySQL container với named volume
docker volume create mysql_test
docker run -d --name mysql_v1 \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=testdb \
  -v mysql_test:/var/lib/mysql \
  mysql:8.0

# 2. Đợi MySQL ready, tạo table + insert data
docker exec -it mysql_v1 mysql -uroot -psecret testdb \
  -e "CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50)); INSERT INTO users (name) VALUES ('Trung'), ('Docker');"

# 3. Xóa container hoàn toàn
docker stop mysql_v1 && docker rm mysql_v1

# 4. Tạo container MỚI, mount cùng volume
docker run -d --name mysql_v2 \
  -e MYSQL_ROOT_PASSWORD=secret \
  -v mysql_test:/var/lib/mysql \
  mysql:8.0

# 5. Kiểm tra → data vẫn còn!
docker exec -it mysql_v2 mysql -uroot -psecret testdb \
  -e "SELECT * FROM users;"
# Output: Trung, Docker → data persist thành công

Bài 2: Bind Mount source code

# 1. Tạo folder và file test
mkdir -p ~/docker-lab/src
echo '<?php echo "Hello from bind mount! Time: " . date("H:i:s");' > ~/docker-lab/src/index.php

# 2. Chạy PHP built-in server với bind mount
docker run -d --name php_bind \
  -p 8080:8080 \
  -v ~/docker-lab/src:/app \
  -w /app \
  php:8.2-cli \
  php -S 0.0.0.0:8080

# 3. Truy cập http://localhost:8080 → thấy output

# 4. Sửa file trên host (KHÔNG restart container)
echo '<?php echo "Updated! Volume works!";' > ~/docker-lab/src/index.php

# 5. Refresh browser → thấy nội dung mới ngay lập tức

Bài 3: Inspect và so sánh

# Xem volume detail
docker volume inspect mysql_test
# → Mountpoint: /var/lib/docker/volumes/mysql_test/_data

# Xem mounts của container
docker inspect mysql_v2 --format 'json .Mounts' | python3 -m json.tool
# → Thấy Type: "volume", Source, Destination

docker inspect php_bind --format 'json .Mounts' | python3 -m json.tool
# → Thấy Type: "bind", Source: host path

Checklist kiểm tra hiểu bài

  • Giải thích được tại sao container mặc định mất data khi bị xóa.
  • Phân biệt được Named Volume vs Bind Mount — khi nào dùng cái nào.
  • Tạo được named volume, mount vào container, xóa container và chứng minh data còn.
  • Bind mount source code và chứng minh thay đổi trên host → container thấy ngay.
  • Hiểu lỗi permission khi bind mount và biết cách fix.
  • Biết cách dùng docker volume ls, inspect, rm, prune.

Kết nối bài tiếp theo

Bạn đã hiểu cách container lưu trữ data persistent. Ở Bài 6: Network, bạn sẽ học cách các container giao tiếp với nhau — đây là mảnh ghép cuối trước khi ghép toàn bộ stack Laravel (PHP + Nginx + MySQL) bằng tay ở Bài 7.

Bài viết liên quan

Đang cập nhật...