❤️ AZDIGI chính thức cập nhật hệ thống blog mới hoàn chỉnh. Tuy nhiên có thể một số bài viết bị sai lệch hình ảnh, hãy ấn nút Báo cáo bài viết ở cuối bài để AZDIGI cập nhật trong thời gian nhanh nhất. Chân thành cám ơn.

Bài 4, bạn đã biết viết Dockerfile và build image riêng cho ứng dụng. Nhưng có một vấn đề mà ai cũng gặp khi làm việc với Docker: xoá container là mất hết dữ liệu. Database, file upload, log,… tất cả đều bay theo container.

Docker Volume và Network
Minh họa: Docker Volume và Network

Và một vấn đề nữa: các container mặc định không biết nhau tồn tại. Muốn WordPress nói chuyện với MySQL? Không đơn giản chỉ là khởi động cả hai cùng lúc.

Trong bài này, mình sẽ hướng dẫn bạn giải quyết cả hai vấn đề với Docker Volume (lưu trữ dữ liệu bền vững) và Docker Network (kết nối container với nhau). Đây là hai kiến thức bắt buộc phải nắm trước khi chạy bất kỳ ứng dụng thực tế nào trên Docker.

Tại sao container lại mất dữ liệu?

Trước khi đi vào giải pháp, bạn cần hiểu gốc rễ vấn đề.

Mỗi container có một writable layer riêng, đây là nơi mọi thay đổi (tạo file, ghi database, cập nhật config) được lưu. Tuy nhiên, layer này gắn chặt với vòng đời của container. Khi bạn chạy docker rm để xoá container, writable layer cũng bị xoá theo. Tất cả dữ liệu bên trong đều biến mất.

Hãy thử để thấy rõ vấn đề:

# Tạo container, ghi một file vào
docker run -d --name test-data alpine sh -c "echo 'Du lieu quan trong' > /data.txt && sleep 3600"
# Kiểm tra — file tồn tại
docker exec test-data cat /data.txt
# Output: Du lieu quan trong
# Xoá container
docker rm -f test-data
# Tạo lại container cùng tên
docker run -d --name test-data alpine sh -c "cat /data.txt 2>/dev/null || echo 'File khong ton tai'"
# Kiểm tra log
docker logs test-data
# Output: File khong ton tai

Dữ liệu đã mất hoàn toàn. Trong thực tế, điều này nghiêm trọng hơn nhiều, tưởng tượng database MySQL của bạn biến mất chỉ vì bạn cần update container lên phiên bản mới.

Docker cung cấp 3 cách để giữ dữ liệu sống sót qua vòng đời container.

3 cách lưu trữ dữ liệu trong Docker

Docker hỗ trợ 3 kiểu storage, mỗi loại phù hợp với trường hợp sử dụng khác nhau:

  • Volume: do Docker quản lý, lưu trong /var/lib/docker/volumes/. Đây là cách được khuyến nghị cho hầu hết trường hợp.
  • Bind mount: map trực tiếp một thư mục/file từ host vào container. Phù hợp khi bạn cần truy cập file từ cả host lẫn container (ví dụ: khi develop).
  • tmpfs mount: lưu dữ liệu trong RAM, không ghi xuống disk. Nhanh nhưng mất khi container stop. Dùng cho dữ liệu tạm hoặc nhạy cảm (session, secret).

Trong bài này, mình sẽ tập trung vào VolumeBind Mount, hai cách bạn sẽ dùng nhiều nhất khi triển khai ứng dụng trên VPS.

Docker Volume – Cách lưu trữ được khuyến nghị

Volume là vùng lưu trữ do Docker tạo và quản lý. Bạn không cần quan tâm nó nằm ở đâu trên host, mọi thứ Docker sẽ lo hết. volume tồn tại độc lập với container. Xoá container, volume vẫn còn.

Các lệnh quản lý volume

# Tạo volume mới
docker volume create my-data

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

# Xem chi tiết volume (đường dẫn, driver, thời gian tạo) docker volume inspect my-data

# Xoá một volume cụ thể docker volume rm my-data

# Xoá TẤT CẢ volumes không được container nào sử dụng docker volume prune

Khi chạy docker volume inspect, bạn sẽ thấy thông tin như:

[
    {
        "CreatedAt": "2025-03-14T10:00:00+07:00",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-data/_data",
        "Name": "my-data",
        "Options": {},
        "Scope": "local"
    }
]

Mountpoint chính là nơi dữ liệu thực sự được lưu trên host. Tuy nhiên, bạn không nên truy cập trực tiếp vào đường dẫn này mà hãy luôn thao tác qua Docker.

Thực hành: MySQL với named volume

Đây là ví dụ thực tế nhất: chạy MySQL container với volume để dữ liệu database không bị mất khi container bị xoá.

Bước 1: Tạo volume và chạy MySQL

# Tạo named volume cho MySQL
docker volume create mysql-data
# Chạy MySQL container, mount volume vào /var/lib/mysql
docker run -d \
  --name my-mysql \
  -e MYSQL_ROOT_PASSWORD=secret123 \
  -e MYSQL_DATABASE=testdb \
  -v mysql-data:/var/lib/mysql \
  mysql:8.0

Ở đây, -v mysql-data:/var/lib/mysql nghĩa là: mount volume có tên mysql-data vào thư mục /var/lib/mysql bên trong container, đây chính là nơi MySQL lưu tất cả data.

Bước 2: Tạo dữ liệu test

# Chờ MySQL khởi động xong (khoảng 10-15 giây)
docker exec -it my-mysql mysql -uroot -psecret123 -e "
  USE testdb;
  CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100));
  INSERT INTO users (name) VALUES ('Thach'), ('Docker Fan');
  SELECT * FROM users;
"

Output:

+----+------------+
| id | name       |
+----+------------+
|  1 | Thach      |
|  2 | Docker Fan |
+----+------------+

Bước 3: Xoá container, tạo lại để kiểm tra data

# Xoá container hoàn toàn
docker rm -f my-mysql

# Tạo container MỚI, cùng volume docker run -d \ --name my-mysql \ -e MYSQL_ROOT_PASSWORD=secret123 \ -v mysql-data:/var/lib/mysql \ mysql:8.0

# Chờ MySQL khởi động, kiểm tra data docker exec -it my-mysql mysql -uroot -psecret123 -e "SELECT * FROM testdb.users;"

Output vẫn là:

+----+------------+
| id | name       |
+----+------------+
|  1 | Thach      |
|  2 | Docker Fan |
+----+------------+

Container đã bị xoá và tạo lại hoàn toàn, nhưng data vẫn nguyên vẹn vì nó nằm trong volume, không phải trong container. Đây chính là lý do bạn luôn cần volume cho bất kỳ container nào có dữ liệu quan trọng.

Bind Mount – Map thư mục từ host vào container

Khác với volume (Docker quản lý), bind mount cho phép bạn chỉ định chính xác thư mục trên host để map vào container. Điều này đặc biệt hữu ích khi phát triển ứng dụng, bạn sửa code trên host và container phản ánh ngay lập tức.

Cú pháp cơ bản

# Cú pháp
-v /đường/dẫn/trên/host:/đường/dẫn/trong/container
# Hoặc cú pháp mới (rõ ràng hơn)
--mount type=bind,source=/đường/dẫn/trên/host,target=/đường/dẫn/trong/container

Điểm khác biệt quan trọng: bind mount dùng đường dẫn tuyệt đối trên host, còn volume chỉ cần tên.

Thực hành: Mount thư mục code vào Nginx

Giả sử bạn đang develop một website tĩnh và muốn xem kết quả ngay trên Nginx mà không cần build lại image mỗi lần sửa file.

# Tạo thư mục project trên host
mkdir -p ~/my-website
echo '<h1>Hello from Docker Volume!</h1>' > ~/my-website/index.html
# Chạy Nginx, mount thư mục code vào
docker run -d \
  --name dev-nginx \
  -p 8080:80 \
  -v ~/my-website:/usr/share/nginx/html \
  nginx

Truy cập http://your-server-ip:8080: bạn sẽ thấy trang “Hello from Docker Volume!”.

Giờ thử sửa file ngay trên host:

# Sửa file trên host
echo '<h1>Updated! Khong can restart container</h1>' > ~/my-website/index.html

Refresh trình duyệt, nội dung cập nhật ngay lập tức, không cần restart hay rebuild gì cả. Đây là workflow cực kỳ phổ biến khi phát triển ứng dụng với Docker.

Read-only mount

Trong một số trường hợp, bạn muốn container chỉ được đọc dữ liệu mà không thể ghi, ví dụ mount file config vào container:

# Thêm :ro ở cuối để mount read-only
docker run -d \
  --name readonly-nginx \
  -p 8081:80 \
  -v ~/my-website:/usr/share/nginx/html:ro \
  nginx

Nếu container cố ghi vào thư mục đó, sẽ bị lỗi Read-only file system. Đây là cách đơn giản để bảo vệ dữ liệu trên host khỏi bị container thay đổi.

So sánh Volume và Bind Mount

Cả hai đều giúp lưu trữ dữ liệu bên ngoài container, nhưng có những khác biệt quan trọng:

Tiêu chíVolumeBind Mount
Quản lý bởiDocker EngineBạn (chỉ định path trên host)
Vị trí trên host/var/lib/docker/volumes/Bất kỳ đường dẫn nào
Tạo tự độngCó (nếu chưa tồn tại)Có (tạo thư mục rỗng nếu path không tồn tại)
Backup / migrateDễ (dùng Docker CLI)Phải tự quản lý
Hiệu năng trên LinuxTốtTốt
Dùng khi nàoDatabase, persistent data, productionDevelopment, config files, share code
Bảo mậtContainer không biết host pathContainer truy cập trực tiếp host filesystem

Nguyên tắc chung: Dùng volume cho production và dữ liệu quan trọng. Dùng bind mount khi develop và cần truy cập file từ cả hai phía.

Docker Network – Kết nối các container với nhau

Giờ bạn đã biết cách giữ dữ liệu. Nhưng một ứng dụng thực tế hiếm khi chỉ có một container, bạn cần web server, database, cache, worker… và chúng phải nói chuyện được với nhau.

Docker Network là cơ chế cho phép các container giao tiếp với nhau an toàn và có kiểm soát. Thay vì dùng IP (có thể thay đổi), container trong cùng network có thể gọi nhau bằng tên.

Các loại network driver

Docker cung cấp 3 network driver chính:

Bridge (mặc định)

Khi bạn chạy docker run mà không chỉ định network, container sẽ được gắn vào network bridge mặc định. Các container trên cùng bridge network có thể giao tiếp với nhau qua IP nội bộ.

Tuy nhiên, bridge mặc định có một hạn chế quan trọng: không hỗ trợ DNS tự động. Muốn container gọi nhau bằng tên, bạn cần tạo custom bridge network (mình sẽ demo ở phần thực hành).

Host

Container sử dụng trực tiếp network stack của host, không có network isolation. Container bind port trực tiếp lên host mà không cần -p.

# Container dùng trực tiếp port 80 của host
docker run -d --network host nginx

Phù hợp khi cần hiệu năng network cao nhất (không qua NAT), nhưng đánh đổi là mất hoàn toàn network isolation. Trên production, hãy cân nhắc kỹ trước khi dùng.

None

Container hoàn toàn không có network. Không giao tiếp được với bên ngoài hay container khác.

# Container không có network
docker run -d --network none alpine sleep 3600

Dùng cho các task không cần network, chạy script xử lý file, build code, hoặc khi cần bảo mật tuyệt đối.

Các lệnh quản lý Docker network

# Liệt kê tất cả networks
docker network ls

# Tạo custom bridge network docker network create my-network

# Xem chi tiết network (subnet, gateway, containers đang kết nối) docker network inspect my-network

# Kết nối container đang chạy vào network docker network connect my-network container-name

# Ngắt kết nối container khỏi network docker network disconnect my-network container-name

# Xoá network docker network rm my-network

# Xoá tất cả networks không được sử dụng docker network prune

Khi chạy docker network ls trên một Docker mới cài, bạn sẽ thấy 3 network mặc định:

NETWORK ID     NAME      DRIVER    SCOPE
a1b2c3d4e5f6   bridge    bridge    local
f6e5d4c3b2a1   host      host      local
1a2b3c4d5e6f   none      null      local

Thực hành: WordPress + MySQL qua custom network

Đây là bài thực hành kết hợp cả VolumeNetwork: triển khai WordPress kết nối với MySQL, giống hệt cách bạn sẽ deploy trên production.

Bước 1: Tạo network và volumes

# Tạo custom bridge network
docker network create wp-network
# Tạo volumes cho MySQL và WordPress
docker volume create wp-db-data
docker volume create wp-content

Tại sao cần custom network thay vì dùng bridge mặc định? Vì custom network hỗ trợ DNS tự động: WordPress có thể gọi MySQL bằng tên container thay vì phải biết IP.

Bước 2: Chạy MySQL container

docker run -d \
  --name wp-mysql \
  --network wp-network \
  -e MYSQL_ROOT_PASSWORD=rootpass123 \
  -e MYSQL_DATABASE=wordpress \
  -e MYSQL_USER=wpuser \
  -e MYSQL_PASSWORD=wppass123 \
  -v wp-db-data:/var/lib/mysql \
  mysql:8.0

Giải thích các tham số:

  • --network wp-network: gắn container vào custom network
  • MYSQL_DATABASE=wordpress: tự động tạo database “wordpress” khi khởi động
  • MYSQL_USER / MYSQL_PASSWORD: tạo user riêng cho WordPress (không dùng root)
  • -v wp-db-data:/var/lib/mysql: lưu data vào volume để không mất khi container bị xoá

Bước 3: Chạy WordPress container

docker run -d \
  --name wp-app \
  --network wp-network \
  -p 8080:80 \
  -e WORDPRESS_DB_HOST=wp-mysql \
  -e WORDPRESS_DB_USER=wpuser \
  -e WORDPRESS_DB_PASSWORD=wppass123 \
  -e WORDPRESS_DB_NAME=wordpress \
  -v wp-content:/var/www/html/wp-content \
  wordpress:latest

Điểm quan trọng nhất ở đây: WORDPRESS_DB_HOST=wp-mysql. WordPress không cần biết IP của MySQL, chỉ cần dùng tên container (wp-mysql) làm hostname. Docker DNS sẽ tự động resolve tên này thành IP đúng.

Bước 4: Truy cập WordPress

Mở trình duyệt và truy cập http://your-server-ip:8080. Bạn sẽ thấy trang cài đặt WordPress, chọn ngôn ngữ, tạo tài khoản admin, và bắt đầu sử dụng.

Kiểm tra cả hai container đều đang chạy trên cùng network:

docker network inspect wp-network --format '{{range .Containers}}{{.Name}} {{end}}'
# Output: wp-mysql wp-app

Kiểm tra data bền vững

Sau khi cài xong WordPress, viết một bài test, rồi thử xoá cả hai container và tạo lại:

# Xoá cả hai container
docker rm -f wp-mysql wp-app

# Tạo lại y hệt (cùng volume, cùng network) docker run -d \ --name wp-mysql \ --network wp-network \ -e MYSQL_ROOT_PASSWORD=rootpass123 \ -v wp-db-data:/var/lib/mysql \ mysql:8.0

docker run -d \ --name wp-app \ --network wp-network \ -p 8080:80 \ -e WORDPRESS_DB_HOST=wp-mysql \ -e WORDPRESS_DB_USER=wpuser \ -e WORDPRESS_DB_PASSWORD=wppass123 \ -e WORDPRESS_DB_NAME=wordpress \ -v wp-content:/var/www/html/wp-content \ wordpress:latest

Truy cập lại http://your-server-ip:8080: WordPress vẫn hoạt động bình thường với đầy đủ dữ liệu, bài viết, theme, plugin. Đây chính là sức mạnh khi kết hợp Volume + Network.

DNS tự động trong custom network

Đây là tính năng mà nhiều người mới dùng Docker không biết, nhưng cực kỳ quan trọng.

Khi bạn tạo custom bridge network, Docker tự động cung cấp một DNS server nội bộ. Mỗi container trên network đó được gán một DNS record với tên = tên container. Nhờ vậy, các container có thể gọi nhau bằng tên thay vì IP.

Hãy thử kiểm chứng:

# Tạo network test
docker network create test-dns

# Chạy 2 container trên cùng network docker run -d --name server-a --network test-dns alpine sleep 3600 docker run -d --name server-b --network test-dns alpine sleep 3600

# Từ server-a, ping server-b bằng TÊN docker exec server-a ping -c 3 server-b

Output:

PING server-b (172.20.0.3): 56 data bytes
64 bytes from 172.20.0.3: seq=0 ttl=64 time=0.089 ms
64 bytes from 172.20.0.3: seq=1 ttl=64 time=0.107 ms
64 bytes from 172.20.0.3: seq=2 ttl=64 time=0.098 ms

server-a có thể gọi server-b bằng tên, Docker tự resolve thành IP 172.20.0.3. Không cần config gì thêm.

Tại sao điều này quan trọng? Vì IP của container có thể thay đổi mỗi lần restart. Nếu bạn hardcode IP, ứng dụng sẽ lỗi. Dùng tên container thì Docker DNS tự cập nhật và container mới sẽ tự có IP mới nhưng vẫn giữ nguyên tên.

Lưu ý: Tính năng DNS tự động chỉ hoạt động trên custom network. Network bridge mặc định không có DNS, đây là lý do bạn nên luôn tạo custom network thay vì dùng bridge mặc định.

# Dọn dẹp
docker rm -f server-a server-b
docker network rm test-dns

📚 Serie Docker từ A đến Z

  1. Bài 1: Docker là gì? Tại sao nên dùng Docker trên VPS
  2. Bài 2: Cài đặt Docker và Docker Compose trên VPS Ubuntu
  3. Bài 3: Làm quen với Docker – Các lệnh cơ bản cần biết
  4. Bài 4: Docker Image & Dockerfile – Tự tạo Image riêng
  5. Bài 5: Docker Volume & Network – Quản lý dữ liệu và mạng (đang đọc)
  6. Bài 6: Docker Compose là gì? Cài đặt và cú pháp cơ bản
  7. Bài 7: Deploy WordPress + MySQL + phpMyAdmin bằng Docker Compose
  8. Bài 8: Deploy LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose
  9. Bài 9: Biến môi trường & file .env trong Docker Compose
  10. Bài 10: Reverse Proxy với Nginx Proxy Manager + SSL tự động
  11. Bài 11: Deploy ứng dụng Node.js / Python với Docker Compose
  12. Bài 12: Backup & Restore dữ liệu Docker Volume
  13. Bài 13: Monitoring Docker với Portainer, Uptime Kuma và cAdvisor
  14. Bài 14: Docker Logging – Quản lý log hiệu quả
  15. Bài 15: Bảo mật Docker trên VPS
  16. Bài 16: CI/CD đơn giản – Auto deploy với Webhook + Docker Compose
  17. Bài 17: Docker Compose trong thực tế – Tổng hợp project mẫu

Tổng kết

Trong bài này, bạn đã học được hai khái niệm cốt lõi để chạy ứng dụng Docker trong thực tế:

Docker Volume:

  • Container mất data khi bị xoá, cần volume hoặc bind mount để lưu trữ bền vững
  • Volume (Docker quản lý) phù hợp cho production và database
  • Bind mount (map thư mục host) phù hợp cho development
  • Luôn dùng named volume cho data quan trọng, xoá container bao nhiêu lần data vẫn còn

Docker Network:

  • 3 loại driver: bridge (giao tiếp giữa containers), host (dùng network host), none (không network)
  • Luôn tạo custom bridge network thay vì dùng bridge mặc định
  • Custom network có DNS tự động: container gọi nhau bằng tên, không cần biết IP
  • Thực hành triển khai WordPress + MySQL với cả Volume lẫn Network

Đến đây, bạn đã nắm vững những thành phần cơ bản của Docker: image, container, Dockerfile, volume, network. Nhưng để chạy một hệ thống như WordPress + MySQL, bạn phải gõ rất nhiều lệnh dài, tạo network, tạo volume, run từng container với hàng loạt tham số.

Bài 6, mình sẽ giới thiệu Docker Compose: công cụ cho phép bạn định nghĩa toàn bộ hệ thống (nhiều container, volume, network) trong một file YAML duy nhất, rồi chạy lên chỉ với một lệnh docker compose up. Tất cả những gì bạn vừa làm thủ công trong bài này sẽ được gói gọn trong vài dòng config.

👈 Bài trước: Docker Image & Dockerfile – Tự tạo Image riêng

👉 Bài tiếp: Docker Compose là gì? Cài đặt và cú pháp cơ bản

Chia sẻ:
Bài viết đã được kiểm duyệt bởi AZDIGI Team

Về tác giả

Thạch Phạm

Thạch Phạm

Đồng sáng lập và Giám đốc điều hành của AZDIGI. Có hơn 15 năm kinh nghiệm trong phổ biến kiến thức liên quan đến WordPress tại thachpham.com, phát triển website và phát triển hệ thống.

Hơn 10 năm phục vụ 80.000+ khách hàng

Bắt đầu dự án web của bạn với AZDIGI