Lưu trữ Dữ liệu Bền vững trong Docker: Mọi Điều Bạn Cần Biết

Chào mừng trở lại với series “Roadmap Docker”! Trong các bài viết trước, chúng ta đã cùng nhau tìm hiểu về Container là gì và vì sao mỗi lập trình viên nên tìm hiểu, phân biệt Container, Máy ảo và Bare Metal, khám phá kiến trúc Docker và tiêu chuẩn OCI, và đi sâu vào các khái niệm cơ bản về Linux cần thiết như Package Managers, Người dùng, Nhóm, Quyền hạn, các lệnh Shell cơ bản, và Shell Scripting. Chúng ta cũng đã chạm đến công nghệ nền tảng như Namespaces, cgroups và UnionFS, và cách cài đặt Docker Engine.

Một trong những đặc điểm cốt lõi của container là tính ngắn hạn (ephemeral). Khi một container bị dừng hoặc xóa, mọi thay đổi được thực hiện trong lớp ghi được (writable layer) của nó sẽ bị mất. Điều này rất tuyệt cho việc khởi tạo nhanh chóng và loại bỏ dễ dàng, nhưng lại là một vấn đề lớn khi ứng dụng của bạn cần lưu trữ dữ liệu, như cơ sở dữ liệu, file cấu hình tùy chỉnh, file người dùng tải lên, hoặc log.

Làm thế nào để dữ liệu “sống sót” sau khi container chết? Đây chính là lúc chúng ta nói về Lưu trữ Dữ liệu Bền vững (Data Persistence) trong Docker. Hiểu rõ cách thức hoạt động và các lựa chọn persistence là cực kỳ quan trọng đối với bất kỳ ai làm việc với Docker, đặc biệt là khi triển khai ứng dụng vào môi trường Production. Bài viết này sẽ đi sâu vào các phương pháp chính mà Docker cung cấp để giải quyết bài toán này.

Tại sao Containers lại không bền vững theo mặc định?

Như chúng ta đã tìm hiểu khi nói về UnionFS, một container Docker được xây dựng từ các lớp ảnh (image layers) chỉ đọc (read-only), chồng lên nhau. Trên cùng của stack này là một lớp ghi được (writable layer) dành riêng cho container đó.

Image Layer 1 (Read-only)
Image Layer 2 (Read-only)
...
Image Layer N (Read-only)
-------------------------
Container Writable Layer (Read/Write)

Khi bạn chạy một container, bất kỳ thao tác nào ghi dữ liệu (tạo file, chỉnh sửa file, xóa file) đều xảy ra trên lớp ghi được này. Đây là lý do tại sao mỗi container, dù được tạo từ cùng một image, lại có thể có trạng thái riêng của nó.

Tuy nhiên, lớp ghi được này gắn liền với lifecycle của container. Khi container bị xóa (`docker rm`), lớp ghi được này cũng biến mất vĩnh viễn, kéo theo mọi dữ liệu bên trong nó. Điều này là tốt cho các tác vụ không trạng thái (stateless), nhưng lại là trở ngại lớn cho các ứng dụng cần lưu trữ trạng thái (stateful).

Để dữ liệu tồn tại độc lập với container, chúng ta cần lưu trữ nó *bên ngoài* hệ thống tệp (filesystem) của container. Docker cung cấp ba cơ chế chính để làm điều này:

  1. Volumes
  2. Bind Mounts
  3. tmpfs Mounts

Hãy cùng khám phá chi tiết từng phương pháp.

Volumes: Giải pháp được Docker khuyến khích

Volumes là cách thức được Docker khuyến khích nhất để lưu trữ dữ liệu bền vững cho các ứng dụng chạy trong container. Docker tự quản lý các volume này trên host server (hoặc hệ thống lưu trữ mạng). Vị trí vật lý của volume trên host thường được ẩn đi (mặc định là trong thư mục `/var/lib/docker/volumes/` trên Linux) và không nên can thiệp trực tiếp vào đó.

Ưu điểm lớn nhất của Volumes là tính quản lýdi động (portability):

  • Quản lý bởi Docker: Docker tự lo việc tạo, quản lý lifecycle và vị trí của volume. Điều này giảm bớt gánh nặng cho người dùng.
  • Di động: Volumes có thể được sử dụng dễ dàng bởi nhiều container và không phụ thuộc vào cấu trúc thư mục cụ thể của host. Chúng cũng là cách tiêu chuẩn để tích hợp với các hệ thống lưu trữ mạng (Network Attached Storage – NAS) hoặc các giải pháp lưu trữ đám mây thông qua Volume Drivers.
  • Hiệu suất: Volumes thường có hiệu suất tốt hơn so với Bind Mounts, đặc biệt trên các hệ điều hành yêu cầu lớp trừu tượng giữa host và container (ví dụ: Docker Desktop trên Windows/macOS sử dụng máy ảo).

Các loại Volumes

Có hai loại volumes chính:

  1. Named Volumes (Volume có tên): Được đặt tên rõ ràng khi tạo hoặc sử dụng. Tên này giúp bạn dễ dàng tham chiếu lại volume đó sau này. Docker sẽ tạo volume nếu nó chưa tồn tại. Dữ liệu trong Named Volumes tồn tại ngay cả khi tất cả container sử dụng nó bị dừng hoặc xóa, cho đến khi volume đó bị xóa thủ công.
  2. Anonymous Volumes (Volume ẩn danh): Không được đặt tên cụ thể. Khi bạn mount một volume mà không chỉ định tên (chỉ định đường dẫn trong container), Docker sẽ tạo một volume với một ID duy nhất. Anonymous Volumes thường ít được sử dụng cho dữ liệu cần persistence lâu dài vì khó quản lý và tham chiếu lại. Chúng có thể bị xóa bởi Docker khi container cuối cùng sử dụng nó bị xóa (tùy thuộc vào cách mount).

Cách sử dụng Volumes

Bạn có thể tạo và quản lý volumes bằng lệnh `docker volume`:

docker volume create mydata

Liệt kê các volumes:

docker volume ls

Xem thông tin chi tiết về một volume:

docker volume inspect mydata

Xóa một volume (chỉ khi không có container nào đang sử dụng):

docker volume rm mydata

Để mount volume vào container khi chạy lệnh `docker run`, bạn sử dụng flag `-v` hoặc `–mount`. Cách dùng `–mount` được khuyến khích hơn vì rõ ràng và chi tiết hơn.

Sử dụng Named Volume với `docker run` (cú pháp `-v`):

docker run -d -p 80:80 \
  --name mywebapp \
  -v mydata:/app/data \
  nginx

Trong ví dụ này, volume có tên `mydata` được mount vào đường dẫn `/app/data` bên trong container `mywebapp`.

Sử dụng Named Volume với `docker run` (cú pháp `–mount`):

docker run -d -p 80:80 \
  --name mywebapp \
  --mount type=volume,source=mydata,target=/app/data \
  nginx

Cú pháp `–mount` minh bạch hơn: `type=volume` chỉ định đây là volume, `source=mydata` là tên volume trên host, và `target=/app/data` là đường dẫn trong container.

Sử dụng Anonymous Volume với `docker run` (cú pháp `-v`):

docker run -d -p 80:80 \
  --name anotherwebapp \
  -v /app/logs \
  ubuntu:latest

Docker sẽ tạo một volume ẩn danh và mount nó vào `/app/logs`. ID của volume này sẽ được hiển thị trong output của `docker inspect anotherwebapp`.

Trong Docker Compose, bạn định nghĩa volumes ở phần root level và sau đó tham chiếu chúng trong service:

# docker-compose.yml
version: '3.8'

volumes:
  db_data: # Định nghĩa named volume

services:
  db:
    image: postgres:latest
    volumes:
      - db_data:/var/lib/postgresql/data # Sử dụng named volume
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password

  webapp:
    image: myapp:latest
    ports:
      - "80:80"
    volumes:
      - ./app/config:/app/config # Bind Mount (sẽ nói sau)
      - data:/app/upload # Ví dụ sử dụng named volume khác hoặc anonymous volume tùy cách Docker Compose xử lý
    depends_on:
      - db

# Docker Compose sẽ tự động tạo named volume 'db_data' nếu nó chưa tồn tại
# Đối với 'data:/app/upload', nếu 'data' không được định nghĩa ở root level, Docker Compose sẽ tạo anonymous volume.
# Nếu muốn rõ ràng là named volume, phải khai báo ở root level volumes: data:

Sử dụng named volumes trong Docker Compose là cách phổ biến nhất để quản lý dữ liệu bền vững cho các service.

Bind Mounts: Liên kết trực tiếp với hệ thống tệp của Host

Bind Mounts cho phép bạn mount trực tiếp một thư mục hoặc một file từ hệ thống tệp của host vào một đường dẫn trong container. Không giống như Volumes, Bind Mounts không được Docker quản lý. Vị trí của dữ liệu trên host hoàn toàn do người dùng kiểm soát.

Bind Mounts rất hữu ích trong các trường hợp sau:

  • Phát triển ứng dụng: Mount mã nguồn cục bộ vào container để bạn có thể chỉnh sửa code trên host và thấy ngay kết quả trong container mà không cần build lại image.
  • Chia sẻ file cấu hình: Mount file cấu hình từ host vào container.
  • Truy cập tài nguyên của host: Mount các file hoặc thư mục của host mà container cần truy cập (ví dụ: `/etc/timezone`, `/etc/localtime`, socket Docker daemon `/var/run/docker.sock`).

Nhược điểm của Bind Mounts:

  • Không di động: Vì nó phụ thuộc vào cấu trúc thư mục của host, một bind mount được thiết lập trên host A có thể không hoạt động trên host B nếu đường dẫn không tồn tại hoặc khác nhau. Điều này gây khó khăn khi di chuyển ứng dụng giữa các môi trường.
  • Vấn đề về quyền (Permissions): Quyền truy cập file và thư mục giữa host và container có thể là một thách thức. Người dùng và nhóm bên trong container cần có quyền phù hợp trên hệ thống tệp của host.
  • Rủi ro bảo mật: Nếu mount nhầm các thư mục nhạy cảm của host vào container, bạn có thể vô tình trao quyền truy cập cho container.
  • Hiệu suất: Hiệu suất của Bind Mounts có thể bị ảnh hưởng, đặc biệt trên các hệ thống sử dụng máy ảo (như Docker Desktop), nơi cần có lớp chia sẻ tệp giữa host và VM.

Cách sử dụng Bind Mounts

Sử dụng Bind Mount với `docker run` (cú pháp `-v`):

docker run -d -p 80:80 \
  --name dev_nginx \
  -v /path/on/host/html:/usr/share/nginx/html \
  nginx

Ở đây, `/path/on/host/html` trên host được mount vào `/usr/share/nginx/html` trong container Nginx.

Sử dụng Bind Mount với `docker run` (cú pháp `–mount`):

docker run -d -p 80:80 \
  --name dev_nginx \
  --mount type=bind,source=/path/on/host/html,target=/usr/share/nginx/html \
  nginx

Cú pháp `–mount` tương tự như Volume, chỉ khác ở `type=bind` và `source` là đường dẫn tuyệt đối hoặc tương đối trên host.

Sử dụng Bind Mount trong Docker Compose:

# docker-compose.yml
version: '3.8'

services:
  webapp:
    image: myapp:latest
    ports:
      - "8080:80"
    volumes:
      - ./src:/app/code # Bind mount thư mục 'src' từ vị trí của docker-compose.yml trên host vào '/app/code' trong container
      - ./config/app.conf:/app/config/app.conf # Bind mount file cấu hình
      - /var/log/myapp:/app/logs # Bind mount một thư mục log cụ thể trên host

Trong Docker Compose, khi `source` bắt đầu bằng `./` hoặc `/`, nó được hiểu là Bind Mount.

tmpfs Mounts: Lưu trữ dữ liệu trong bộ nhớ Host (RAM)

tmpfs Mount cho phép mount một thư mục vào hệ thống tệp của container nhưng dữ liệu thực tế được lưu trữ trong bộ nhớ (RAM) của host. Dữ liệu này sẽ không bền vững; nó sẽ biến mất khi container bị dừng hoặc host khởi động lại.

tmpfs Mounts hữu ích cho các trường hợp:

  • Lưu trữ dữ liệu nhạy cảm tạm thời: Ví dụ: key/certificate, session data mà bạn không muốn ghi xuống ổ đĩa.
  • Cải thiện hiệu suất cho dữ liệu tạm: Các ứng dụng tạo ra lượng lớn dữ liệu tạm thời, không cần lưu lại sau khi container kết thúc. Ghi vào RAM nhanh hơn nhiều so với ghi vào ổ đĩa.
  • Giảm tải ghi/đọc cho ổ đĩa SSD: Đối với các ứng dụng ghi/đọc rất nhiều file tạm, sử dụng tmpfs có thể kéo dài tuổi thọ của ổ đĩa SSD.

Lưu ý rằng dữ liệu trong tmpfs chỉ giới hạn bởi lượng RAM có sẵn trên host. Sử dụng tmpfs quá nhiều hoặc lưu trữ dữ liệu quá lớn có thể dẫn đến việc host sử dụng swap memory, ảnh hưởng nghiêm trọng đến hiệu suất.

Cách sử dụng tmpfs Mounts

tmpfs Mount chỉ có thể được sử dụng với cú pháp `–mount` khi chạy lệnh `docker run`:

docker run -d \
  --name mycontainer \
  --mount type=tmpfs,target=/app/temp \
  ubuntu:latest

Lệnh này sẽ tạo một tmpfs mount tại `/app/temp` trong container. Bạn có thể tùy chọn giới hạn kích thước hoặc các tùy chọn mount khác:

docker run -d \
  --name mycontainer_limited \
  --mount type=tmpfs,target=/app/temp,tmpfs-size=100m,tmpfs-mode=755 \
  ubuntu:latest

Trong ví dụ này, tmpfs mount được giới hạn kích thước là 100MB và quyền là 755.

Trong Docker Compose, bạn sử dụng loại mount là `tmpfs`:

# docker-compose.yml
version: '3.8'

services:
  my_service:
    image: myapp:latest
    ports:
      - "80:80"
    tmpfs: # Cách viết shorthand cho tmpfs mounts
      - /app/cache # Mount tmpfs tại /app/cache
      - /app/tmp:size=50m,mode=777 # Mount tmpfs tại /app/tmp với kích thước 50MB và quyền 777

  another_service:
    image: anotherapp:latest
    volumes: # Sử dụng cú pháp volumes với type: tmpfs
      - type: tmpfs
        target: /data/temp
        tmpfs:
          size: 200m
          mode: 1777 # Quyền 1777 giống /tmp trên Linux (sticky bit)

Docker Compose hỗ trợ cả cú pháp shorthand `tmpfs` và cú pháp chi tiết hơn trong phần `volumes` với `type: tmpfs`.

Chọn phương pháp lưu trữ phù hợp

Việc lựa chọn giữa Volumes, Bind Mounts và tmpfs Mounts phụ thuộc vào mục đích sử dụng và yêu cầu về tính bền vững của dữ liệu. Dưới đây là bảng so sánh tóm tắt:

Tính năng Volumes Bind Mounts tmpfs Mounts
Tính bền vững Có (Sống sót sau khi container bị xóa, cho đến khi volume bị xóa) Có (Sống sót độc lập với container, dữ liệu nằm trên host) Không (Dữ liệu mất khi container dừng hoặc host tắt)
Vị trí lưu trữ Được Docker quản lý trên host (thường là `/var/lib/docker/volumes/`) hoặc hệ thống lưu trữ mạng Đường dẫn cụ thể do người dùng chỉ định trên host Bộ nhớ RAM của host
Được Docker quản lý Không Có (Mounting & lifecycle gắn với container)
Tính di động Cao (Không phụ thuộc vào cấu trúc thư mục host, hỗ trợ Volume Drivers) Thấp (Phụ thuộc vào cấu trúc thư mục host) Cao (Không phụ thuộc vào cấu trúc thư mục host, chỉ cần RAM)
Hiệu suất Thường tốt hơn bind mounts, đặc biệt trên Docker Desktop Có thể chậm hơn volumes, đặc biệt trên Docker Desktop, có thể bị ảnh hưởng bởi hiệu suất của ổ đĩa host Rất nhanh (truy cập RAM)
Trường hợp sử dụng chính Dữ liệu ứng dụng (Cơ sở dữ liệu, dữ liệu người dùng), chia sẻ dữ liệu giữa các container Phát triển (Mount code nguồn), mount file/thư mục cấu hình của host, truy cập tài nguyên host Dữ liệu nhạy cảm tạm thời, cache, dữ liệu hiệu suất cao không cần bền vững
Khả năng chia sẻ Có (Nhiều container có thể mount cùng volume) Có (Nhiều container có thể bind mount cùng đường dẫn trên host) Không (Mỗi container có tmpfs mount riêng)

Lời khuyên chung:

  • Sử dụng Volumes cho hầu hết các trường hợp cần lưu trữ dữ liệu bền vững của ứng dụng trong Production (cơ sở dữ liệu, file upload của người dùng, v.v.). Đây là phương pháp tiêu chuẩn, an toàn và di động nhất.
  • Sử dụng Bind Mounts chủ yếu trong môi trường Phát triển để workflow code-test nhanh chóng, hoặc khi bạn cần chia sẻ các file cấu hình *cụ thể của host* vào container. Hạn chế sử dụng trong Production nếu không thật sự cần thiết do vấn đề di động và quyền.
  • Sử dụng tmpfs Mounts cho dữ liệu tạm thời, không nhạy cảm cần tốc độ truy cập cao hoặc dữ liệu nhạy cảm mà không cần lưu lại trên đĩa vật lý. Tuyệt đối không dùng cho dữ liệu cần bền vững.

Các lưu ý và Best Practices

  1. Quyền hạn (Permissions): Đặc biệt khi sử dụng Bind Mounts, hãy cẩn thận với quyền của người dùng/nhóm mà container sử dụng so với quyền trên host filesystem. Thiết lập người dùng không phải root trong container và đảm bảo người dùng đó có quyền ghi/đọc phù hợp trên host là rất quan trọng để tránh các lỗi “Permission Denied”.
  2. Back up Volumes: Dữ liệu trong Volumes là bền vững, nhưng nó vẫn nằm trên host server. Bạn cần có chiến lược backup dữ liệu trong volumes ra nơi khác (ví dụ: S3, hệ thống backup tập trung) để phòng trường hợp host gặp sự cố.
  3. Volume Drivers: Đối với các ứng dụng Production yêu cầu tính sẵn sàng cao và khả năng mở rộng, hãy khám phá các Volume Drivers cho phép lưu trữ volumes trên các hệ thống lưu trữ mạng tập trung (như NFS, iSCSI, hoặc các dịch vụ lưu trữ đám mây như AWS EBS, Azure Disk, Google Persistent Disk). Điều này giúp decoupling dữ liệu khỏi một host cụ thể.
  4. Đừng lưu dữ liệu quan trọng trong lớp ghi của container: Lặp lại lần nữa cho chắc chắn: Mọi thứ bạn ghi *trực tiếp* vào các thư mục không được mount trong container sẽ bị mất khi container bị xóa.
  5. Hiểu rõ Lifecycle: Volumes (đặc biệt là Named Volumes) có lifecycle độc lập với container. Chúng sẽ không bị xóa khi container bị xóa (trừ khi bạn thêm cờ `–rm` khi chạy container và volume đó là anonymous volume được tạo cùng container đó). Hãy chủ động quản lý và dọn dẹp các volume không còn sử dụng để tránh lãng phí dung lượng đĩa.
  6. Sử dụng Docker Compose/Swarm/Kubernetes: Khi làm việc với các ứng dụng phức tạp có nhiều service, hãy luôn định nghĩa và sử dụng volumes/mounts thông qua Docker Compose hoặc các công cụ điều phối (orchestration) như Docker Swarm hay Kubernetes. Điều này giúp quản lý cấu hình persistence một cách tập trung và nhất quán.

Kết luận

Hiểu và sử dụng thành thạo các cơ chế lưu trữ dữ liệu bền vững là một kỹ năng nền tảng cho bất kỳ kỹ sư DevOps hay nhà phát triển nào làm việc với Docker. Volumes, Bind Mounts và tmpfs Mounts mỗi loại có ưu nhược điểm và trường hợp sử dụng riêng.

Volumes là lựa chọn mặc định và tốt nhất cho hầu hết các ứng dụng cần dữ liệu bền vững trong môi trường Production nhờ khả năng quản lý, tính di động và hỗ trợ driver. Bind Mounts hữu ích cho quá trình phát triển và mount cấu hình host. tmpfs Mounts dành cho dữ liệu tạm thời trong bộ nhớ.

Nắm vững cách sử dụng và khi nào nên sử dụng từng loại sẽ giúp bạn xây dựng các ứng dụng container hóa mạnh mẽ, tin cậy và dễ quản lý hơn. Khi tiến sâu hơn vào Docker Roadmap, chúng ta sẽ thấy các công cụ orchestration như Docker Swarm và Kubernetes xây dựng mạnh mẽ dựa trên khái niệm Volumes để quản lý trạng thái của ứng dụng phân tán.

Trong bài viết tiếp theo, chúng ta sẽ chuyển sang một chủ đề quan trọng khác: Networking trong Docker. Làm thế nào để các container giao tiếp với nhau và với thế giới bên ngoài? Hẹn gặp lại trong phần tiếp theo của series!

Chỉ mục