Bài 03

Network & Storage: kết nối container và giữ data

13 tháng 5, 2026 · 120 phút đọc

Bài 03 — Network & Storage: kết nối container và giữ data

Bài 01-02 bạn đã tạo được container và image custom. Giờ là lúc giải quyết 2 vấn đề cốt lõi:

  1. Network — làm sao container giao tiếp với nhau và với thế giới?
  2. Storage — làm sao giữ data khi container bị xóa?

Nếu bài 02 dạy "đóng gói app", bài này dạy "vận hành stack" — multiple container kết nối + persist data. Sau bài này bạn sẽ dựng được full stack (web + database + cache) trên Docker mà data không mất.

Bài chia 2 phần lớn:

  • Phần A — Docker Network
  • Phần B — Docker Storage

Phần A — Docker Network

Vấn đề: container giao tiếp thế nào?

3 câu hỏi cần trả lời:

  1. Container → ngoài internet: container web call API Google được không?
  2. Ngoài → container: user từ internet truy cập web container như thế nào?
  3. Container ↔ Container: web container connect đến database container như thế nào?

Mỗi câu trả lời cần 1 loại network khác nhau. Docker cung cấp 5 driver mạng để cover mọi case.

                Internet (8.8.8.8)
                       ▲
                       │ ❓ container ra ngoài?
                       │
                ┌──────┴──────────┐
                │   Docker Host   │
                │                  │
   ❓ user vào? │ ┌───────────────┐│
   ────────────┤►│ Container Web ││
                │ └───────┬───────┘│
                │         │         │
                │  ❓ web ↔ db?    │
                │         │         │
                │ ┌───────▼───────┐│
                │ │ Container DB  ││
                │ └───────────────┘│
                └──────────────────┘

5 loại Network Driver

┌──────────────────────────────────────────────────────────────┐
│                  5 Docker Network Drivers                     │
├─────────────┬────────────────────────────────────────────────┤
│ bridge      │ Default. Container có IP riêng, NAT ra ngoài.   │
│             │ Use case: hầu hết trường hợp.                   │
├─────────────┼────────────────────────────────────────────────┤
│ host        │ Container DÙNG TRỰC TIẾP network host.          │
│             │ Use case: high performance, port host = port    │
│             │ container.                                       │
├─────────────┼────────────────────────────────────────────────┤
│ none        │ Container KHÔNG có network (chỉ loopback).      │
│             │ Use case: batch job offline, security extreme.  │
├─────────────┼────────────────────────────────────────────────┤
│ overlay     │ Multi-host network (Docker Swarm).              │
│             │ Use case: container chạy trên nhiều host khác   │
│             │ nhau, giao tiếp như cùng mạng.                  │
├─────────────┼────────────────────────────────────────────────┤
│ macvlan     │ Container có IP RIÊNG trên mạng vật lý.         │
│             │ Use case: app yêu cầu IP thật, legacy network.  │
└─────────────┴────────────────────────────────────────────────┘

1. Bridge Network (mặc định)

Khi tạo container mà không chỉ định network, Docker tự động gán vào bridge network mặc định (tên bridge).

                Internet
                   ▲
                   │ NAT (qua iptables)
                   │
              ┌────┴─────┐
              │ Host eth0│  IP: 192.168.1.100
              └────┬─────┘
                   │
              ┌────┴─────────────────────────────┐
              │      docker0 (bridge interface)  │
              │      IP: 172.17.0.1              │
              └────┬─────────┬─────────┬─────────┘
                   │         │         │
              ┌────┴────┐ ┌──┴─────┐ ┌─┴──────┐
              │Container│ │Container│ │Container│
              │ 172.17. │ │ 172.17. │ │ 172.17. │
              │  0.2    │ │  0.3    │ │  0.4    │
              │ (nginx) │ │ (mysql) │ │ (redis) │
              └─────────┘ └─────────┘ └─────────┘

              Mỗi container có IP riêng trong dải 172.17.0.0/16
              Ra internet: NAT qua host eth0
              User truy cập từ ngoài: cần -p port mapping

Đặc điểm:

  • Container có IP riêng trong dải 172.17.0.0/16 (subnet bridge default).
  • Container talk với nhau qua docker0 bridge interface.
  • Ra internet: NAT port qua host.
  • Vào từ ngoài: phải -p HOST:CONTAINER mapping.

⚠️ Nhiều container cùng default bridge không thấy nhau bằng tên (DNS không work). Phải dùng IP — không tin cậy vì IP thay đổi. Xem section "Custom Bridge" dưới.


2. Host Network

Loại bỏ lớp network isolation — container SỬ DỤNG TRỰC TIẾP mạng của host (dùng luôn IP của host).

                Internet
                   ▲
                   │
              ┌────┴────────┐
              │  Host eth0  │  IP: 192.168.1.100
              │             │
              │ ┌─────────┐ │
              │ │Container│ │  ← container dùng TRỰC TIẾP
              │ │  nginx  │ │     network của host
              │ │ port 80 │ │     port container = port host
              │ └─────────┘ │
              └─────────────┘

              Container nginx listen port 80
              → user truy cập http://192.168.1.100 vào container thẳng
              → KHÔNG cần -p 80:80
# Run với host network
docker run -d --network host nginx
# Container nginx tự bind port 80 của host
# Không cần -p, không có NAT

Đặc điểm:

  • Hiệu suất cao — không có NAT overhead.
  • Port container = port host — đơn giản.
  • Dễ xung đột port — chỉ 1 container dùng port 80, container thứ 2 nginx fail vì port đã bị chiếm.
  • Không có isolation — container thấy mọi interface của host.

Use case: app performance-critical (HFT trading, video streaming) hoặc khi cần access raw network của host (network sniffer, monitoring agent).


3. None Network

Container KHÔNG có network — chỉ có loopback interface (127.0.0.1).

              ┌─────────────────┐
              │   Container     │
              │  ┌────────────┐ │
              │  │ lo (127.0  │ │  ← chỉ có loopback
              │  │      .0.1) │ │
              │  └────────────┘ │
              │                  │
              │   (KHÔNG có eth0)│
              │   (Không ra      │
              │    internet)     │
              └─────────────────┘
docker run --network none alpine ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP> ...
#     inet 127.0.0.1/8 scope host lo
# (không có eth0)

Use case:

  • Batch job xử lý local data, không cần network.
  • Security cực nghiêm ngặt — container chạy code không tin cậy.
  • Test isolation.

4. Overlay Network (Docker Swarm)

Dùng để kết nối container chạy trên các host khác nhau trong cụm Docker Swarm.

              Host A (Swarm Node)         Host B (Swarm Node)
        ┌─────────────────────┐    ┌─────────────────────┐
        │ ┌────────┐ ┌──────┐ │    │ ┌────────┐ ┌──────┐ │
        │ │Web-1   │ │DB-1  │ │    │ │Web-2   │ │Cache │ │
        │ │10.0.0.2│ │10.0.0│ │    │ │10.0.0.4│ │10.0.0│ │
        │ │         │ │.3   │ │    │ │         │ │.5    │ │
        │ └────┬───┘ └──┬───┘ │    │ └────┬───┘ └──┬───┘ │
        └──────┼────────┼─────┘    └──────┼────────┼─────┘
               │        │                  │        │
               └────────┼──────────────────┼────────┘
                        │   Overlay (VXLAN)        │
                        │   10.0.0.0/24            │
                        └──────────────────────────┘

              4 container ở 2 host khác nhau,
              nhưng cùng overlay network → giao tiếp như cùng LAN
              KHÔNG cần NAT

Đặc điểm:

  • Multi-host networking — container ở host A talk với container ở host B trực tiếp.
  • Không cần NAT giữa các host.
  • Cần Docker Swarm hoặc Kubernetes mode.
  • Built-in encryption (optional).

⚠️ Overlay phức tạp hơn các loại khác. Production thường dùng K8s thay Swarm → cần học K8s networking riêng.


5. Macvlan Network

Cho phép container có địa chỉ IP RIÊNG trên mạng vật lý — giống như 1 máy thực.

            Mạng vật: 192.168.1.0/24
        ───────┬───────┬───────┬───────┬────────
               │       │       │       │
            ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
            │ PC  │ │Host │ │Cont1│ │Cont2│
            │.10  │ │.100 │ │.101 │ │.102 │  ← container có IP
            └─────┘ └─────┘ └─────┘ └─────┘    riêng trên LAN

       Router/switch không phân biệt được
       container và máy vật
# Tạo macvlan network
docker network create -d macvlan \
    --subnet=192.168.1.0/24 \
    --gateway=192.168.1.1 \
    -o parent=eth0 \
    my_macvlan

# Container có IP thực trên LAN
docker run --network my_macvlan --ip 192.168.1.101 nginx

Use case:

  • App yêu cầu IP thật (vd: monitoring agent cần broadcast trên LAN).
  • Legacy app không support reverse proxy.
  • Tương thích với DHCP/firewall theo IP.

⚠️ Nhiều DC/cloud cấm macvlan (security policy). Production cần check trước.


So sánh nhanh 5 driver

Driver Scope IP NAT Port mapping Use case
bridge 1 host Riêng (172.17.x.x) Cần -p Default, most cases
host 1 host = IP host Không cần -p High performance
none 1 host N/A Offline batch
overlay Multi-host Riêng (10.0.x.x) Service publish Swarm/K8s
macvlan 1 host Trên LAN vật lý Truy cập trực tiếp Legacy, special

Network Commands

# Liệt kê network
docker network ls
# NETWORK ID   NAME      DRIVER    SCOPE
# abc123       bridge    bridge    local
# def456       host      host      local
# ghi789       none      null      local

# Kiểm tra cấu hình network (xem container nào connect, IP, subnet)
docker network inspect bridge

# Tạo custom network
docker network create my-network
docker network create --driver bridge --subnet 10.5.0.0/16 my-app-net

# Connect container vào network
docker network connect my-network my-container

# Disconnect
docker network disconnect my-network my-container

# Xóa network
docker network rm my-network

Default Bridge vs Custom Bridge — quan trọng

Đây là điểm nhiều người newbie không biết, gây bug ngu:

Default Bridge (network "bridge"):    Custom Bridge (network "my-net"):
┌────────────────────────────┐         ┌────────────────────────────┐
│ - DNS giữa container:       │         │ - DNS giữa container:       │
│   ❌ KHÔNG work             │         │   ✅ Work qua --name        │
│                              │         │                              │
│ - Phải dùng IP để kết nối   │         │ - Container ping bằng tên    │
│   (IP có thể đổi)           │         │   (stable, không đổi)        │
│                              │         │                              │
│ - Container auto join        │         │ - Phải explicit join        │
│   (docker run không -net)   │         │   --network my-net          │
└────────────────────────────┘         └────────────────────────────┘

Demo

# DEFAULT BRIDGE — DNS không work
docker run -d --name web nginx
docker run --rm alpine ping web -c 2
# ping: bad address 'web'    ← FAIL

# CUSTOM BRIDGE — DNS work
docker network create my-net
docker run -d --name web --network my-net nginx
docker run --rm --network my-net alpine ping web -c 2
# 64 bytes from web (172.18.0.2)    ← OK

Production luôn dùng custom bridge. Đừng dùng default bridge cho app thực.


DNS giữa container — magic của Docker

Trong custom bridge network, container talk với nhau qua TÊN (--name), không qua IP.

              Custom Network "app-net"
        ┌────────────────────────────────────┐
        │                                      │
        │  ┌──────┐         ┌──────┐          │
        │  │ web  │────────►│  db  │          │
        │  │      │  ping db│      │          │
        │  └──────┘         └──────┘          │
        │                                      │
        │  Docker DNS resolver tự động:        │
        │  "db" → 172.18.0.3 (IP của container│
        │         "db" trong network app-net)  │
        └────────────────────────────────────┘
# Tạo network
docker network create app-net

# Run 2 container trong cùng network
docker run -d --name db --network app-net mysql:8
docker run -d --name web --network app-net nginx

# Trong web, ping db
docker exec web ping db -c 2
# 64 bytes from db (172.18.0.2): icmp_seq=1 ...   ← OK

→ App config có thể hardcode db (tên container) thay vì IP. Container restart đổi IP, nhưng tên không đổi → app vẫn work.

--network-alias — Load Balancing DNS

Nhiều container có thể chia sẻ 1 alias name → DNS round-robin giữa các container = load balancing built-in.

              Custom Network "app-net"
        ┌────────────────────────────────────┐
        │                                      │
        │       Alias: "web"                   │
        │  ┌──────┐  ┌──────┐  ┌──────┐       │
        │  │web-1 │  │web-2 │  │web-3 │       │
        │  │      │  │      │  │      │       │
        │  └──────┘  └──────┘  └──────┘       │
        │       ▲       ▲       ▲              │
        │       │       │       │              │
        │       └───┬───┴───┬───┘              │
        │           │       │                  │
        │  ping web → DNS round-robin          │
        │  lần 1: web-1, lần 2: web-2, ...    │
        └────────────────────────────────────┘
docker network create app-net

# 3 container web, cùng alias "web"
docker run -d --name web-1 --network app-net --network-alias web nginx
docker run -d --name web-2 --network app-net --network-alias web nginx
docker run -d --name web-3 --network app-net --network-alias web nginx

# Ping "web" — round-robin giữa 3 container
docker run --rm --network app-net alpine sh -c "
for i in 1 2 3 4 5; do
    ping -c 1 web | head -1
done
"
# PING web (172.18.0.2): ...   ← web-1
# PING web (172.18.0.3): ...   ← web-2
# PING web (172.18.0.4): ...   ← web-3
# PING web (172.18.0.2): ...   ← web-1 (round-robin)

Load balancing đơn giản chỉ qua DNS. Không cần nginx LB cho internal traffic.


Container kết nối nhiều Network

1 container có thể connect đồng thời nhiều network — pattern phổ biến cho microservices: tách traffic frontend ↔ backend.

                     ┌───────────────────────────────┐
                     │   Container "web"             │
                     │   (kết nối 2 network)         │
                     └────────┬──────────────┬───────┘
                              │              │
                              │              │
                ┌─────────────┴──┐      ┌────┴──────────┐
                │                 │      │                │
                │ Network         │      │  Network       │
                │ "frontend-net"  │      │  "backend-net" │
                │ 172.20.0.0/16   │      │  172.21.0.0/16 │
                │                 │      │                │
                │ ┌─────────────┐ │      │ ┌────────────┐ │
                │ │ container   │ │      │ │ container  │ │
                │ │ user-facing │ │      │ │ db, cache  │ │
                │ │ (lb, api-gw)│ │      │ │ (internal) │ │
                │ └─────────────┘ │      │ └────────────┘ │
                └─────────────────┘      └────────────────┘
                  (public-ish)              (private)
# Tạo 2 network tách biệt
docker network create frontend-net
docker network create backend-net

# Run db chỉ trong backend (private)
docker run -d --name db --network backend-net mysql:8

# Run web kết nối CẢ 2 network
docker run -d --name web --network frontend-net nginx
docker network connect backend-net web      # ← thêm network thứ 2

# Run load balancer chỉ trong frontend (public)
docker run -d --name lb --network frontend-net haproxy

# Test:
docker exec web ping db       # ✅ OK (cùng backend-net)
docker exec web ping lb       # ✅ OK (cùng frontend-net)
docker exec lb ping db        # ❌ FAIL (khác network)

Pattern này quan trọng:

  • DB không expose ra public — chỉ container app (web) thấy.
  • Load balancer không thấy DB — security boundary.
  • Web đứng giữa 2 network, làm cầu nối.

→ Đây là microservices pattern chuẩn. Sẽ thấy lại trong Kubernetes (NetworkPolicy).


Immutable Infrastructure — không sửa container, deploy lại

Đây là mindset quan trọng nhất của container. Tài liệu của bạn ghi:

"với container không thay đổi chỉ re-deploy"

❌ Mindset cũ (mutable):              ✅ Mindset container (immutable):
                                       
   VM/Server cũ:                          Container:
   ┌──────────────┐                       ┌──────────────┐
   │ App v1.0     │                       │ Image v1.0   │  ← deploy
   │              │ ──update──►           └──────┬───────┘
   │ → patch     │                                │
   │ → config sửa│                                │ build image mới
   │ → log tích   │                                ▼
   │   tụ         │                       ┌──────────────┐
   │              │                       │ Image v1.1   │
   └──────────────┘                       └──────┬───────┘
   "Pet" — nuôi                                  │
   chăm sóc lâu dài                              │ docker rm v1.0
                                                  │ docker run v1.1
                                                  ▼
                                          ┌──────────────┐
                                          │ Container    │
                                          │ v1.1 (mới)   │
                                          └──────────────┘
                                          "Cattle" — thay
                                          mới khi cần

Quy tắc immutable:

  • Container không bao giờ "update". Code đổi → build image v1.1 → deploy container mới.
  • Container không "patch" — security update? Build image mới, deploy.
  • Config không sửa trong container — đổi env var hoặc volume → deploy mới.

Lợi ích:

  • Reproducible — image v1.1 build đâu cũng giống nhau.
  • Rollback dễ — fail thì pull image v1.0, deploy lại.
  • No "configuration drift" — server không trở nên "đặc biệt" theo thời gian.
  • Disaster recovery — server cháy? Pull image, deploy. Như mới.

Quy tắc: container là cattle, not pet. Nuôi đàn, không nuôi con. Cần thì thay con mới.


Phần B — Docker Storage

Vấn đề: container ephemeral

Container là 1 layer trong image. Khi container bị xóa, dữ liệu trong container cũng mất đi.

┌──────────────────────────────────────┐
│   Container (running)                 │
│                                        │
│   ┌──────────────────────────┐        │
│   │ Writable Layer            │        │
│   │ ├── /var/lib/mysql/        │        │
│   │ │   ├── ibdata1            │  ← 8GB │
│   │ │   ├── mysql.db           │     data│
│   │ │   └── ...                │        │
│   │ ├── /tmp/                  │        │
│   │ └── /home/                 │        │
│   └──────────────────────────┘        │
│                                        │
│   ┌──────────────────────────┐        │
│   │ Image layers (read-only) │        │
│   └──────────────────────────┘        │
└──────────────────────────────────────┘

                  │
                  │ docker rm
                  ▼
              ┌───────┐
              │  💥   │  ← Writable layer mất
              │  GONE │     8GB data mất sạch
              └───────┘

⚠️ Container chạy database mà không có persistent storage = catastrophe. Restart container = mất hết data.

→ Cần map data ra ngoài container, bộ nhớ trên host.


Persistent Data — 3 cách

Persistent Data giúp lưu trữ dữ liệu mà không bị mất khi container dừng/xóa.

Docker có 3 phương pháp:

        ┌──────────────────────────────┐
        │      Persistent Storage       │
        └────────────┬─────────────────┘
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
   ┌─────────┐  ┌─────────┐  ┌─────────┐
   │ Volume  │  │  Bind   │  │  tmpfs  │
   │         │  │  Mount  │  │         │
   │ Docker  │  │ Host    │  │ RAM     │
   │ managed │  │ disk    │  │ (tạm)   │
   └─────────┘  └─────────┘  └─────────┘
Phương pháp Storage Quản lý Tốc độ Persistent
Volume Host disk (/var/lib/docker/volumes/) Docker Trung bình ✅ Yes
Bind Mount Host disk (đường dẫn tùy ý) Bạn Trung bình ✅ Yes
tmpfs RAM Docker Rất nhanh ❌ No (mất khi container die)

1. Volume — Docker quản lý

Volume là phương pháp được Docker quản lý hoàn toàn:

  • Docker tạo, sửa, xóa volume.
  • Lưu ở 1 đường dẫn cố định: /var/lib/docker/volumes/<name>/_data.
  • Host không thấy data trực tiếp (trừ khi vào đúng path Docker).
  • Bảo mật và quyền truy cập do Docker kiểm soát.
                ┌──────────────────────────────────┐
                │       Docker Host                 │
                │                                    │
                │  /var/lib/docker/volumes/         │
                │   └── mydata/                     │
                │        └── _data/   ← data thật ở đây
                │             ├── file1            │
                │             ├── file2            │
                │             └── ...              │
                │                                    │
                │  ┌──────────────────────┐         │
                │  │   Container          │         │
                │  │                       │         │
                │  │  /app/data ──────────┼─────────┘  ← mount point
                │  │  (trong container)   │
                │  └──────────────────────┘
                └────────────────────────────────────┘

                Data ghi vào /app/data trong containerthực ra ghi vào /var/lib/docker/volumes/mydata/_data trên host

Tạo và dùng Volume

# Tạo volume
docker volume create mydata

# Gắn volume vào container
docker run -d \
    -v mydata:/data \
    --name my-container \
    nginx
#   -v <volume-name>:<path-trong-container>

→ Mọi thứ container ghi vào /data được lưu vào volume mydata. Container die → data còn → tạo container mới mount lại → data nguyên vẹn.

Volume Commands

# Liệt kê volume
docker volume ls
# DRIVER    VOLUME NAME
# local     mydata

# Xem chi tiết (vị trí trên host, driver, mount point)
docker volume inspect mydata
# [
#     {
#         "Driver": "local",
#         "Mountpoint": "/var/lib/docker/volumes/mydata/_data",
#         "Name": "mydata",
#         ...
#     }
# ]

# Xóa volume
docker volume rm mydata

# Xóa mọi volume không dùng
docker volume prune

Cú pháp -v vs --mount

# Cũ: -v (short syntax)
docker run -v mydata:/data nginx

# Mới: --mount (explicit syntax, recommend)
docker run --mount type=volume,source=mydata,target=/data nginx

--mount rõ ràng hơn, ít lỗi hơn. Modern Docker khuyến cáo dùng --mount.

⚠️ Docker không hỗ trợ gắn volume vào container đang chạy. Phải stop + recreate container.

Backup & Restore Volume

   BACKUP FLOW:
   
   ┌─────────────────┐               ┌─────────────────┐
   │  Source Volume  │               │   Host disk     │
   │  "mydata"       │               │   (pwd)         │
   │                 │               │                  │
   │  /var/lib/      │               │  ./backup.tar   │
   │   docker/       │               │   .gz           │
   │   volumes/      │               │                  │
   └────────┬────────┘               └────────▲────────┘
            │                                  │
            │ mount -v mydata:/data            │ mount -v $(pwd):/backup
            │                                  │
            └───────┬──────────────────────────┘
                    ▼
            ┌────────────────────┐
            │  Alpine container   │
            │  (tạm thời)         │
            │                     │
            │  tar czf            │
            │  /backup/file.tar.gz│
            │  -C /data .         │
            │                     │
            │  Auto delete (--rm) │
            └────────────────────┘

   RESTORE FLOW: ngược lại
   
   ┌─────────────────┐               ┌─────────────────┐
   │  New Volume     │               │   Host disk     │
   │  "new_vol"      │◄───tar xzf────│  ./backup.tar.gz │
   │  (empty)        │               │                  │
   └─────────────────┘               └─────────────────┘
                          (qua alpine container)
# Backup volume "mydata" thành file tar
docker run --rm \
    -v mydata:/data \
    -v $(pwd):/backup \
    alpine \
    tar czf /backup/my_backup.tar.gz -C /data .

# File my_backup.tar.gz xuất hiện ở thư mục hiện tại
ls -la my_backup.tar.gz

# Restore vào volume mới
docker volume create new_volume
docker run --rm \
    -v new_volume:/data \
    -v $(pwd):/backup \
    alpine \
    tar xzf /backup/my_backup.tar.gz -C /data

Trick: dùng container alpine tạm thời để tar/untar volume. Container chạy xong tự xóa (--rm).

Khi nào dùng Volume

  • Database data (MySQL, PostgreSQL, Redis).
  • ✅ Cần persistent, không quan tâm chi tiết file trên host.
  • ✅ Chạy trên nhiều môi trường (Linux, Mac, Windows — volume work mọi nơi).
  • ✅ Cần Docker quản lý lifecycle.
  • ❌ Không dễ chỉnh sửa file từ host (cần docker cp hoặc docker exec).
  • ❌ Không trực quan cho develop (sửa code phải build lại).

2. Bind Mount — Host quản lý

Bind Mount là cách gắn 1 thư mục/file CỦA HOST vào container. Host và container chia sẻ data trực tiếp và đồng bộ ngay lập tức.

                ┌──────────────────────────────────┐
                │       Docker Host                 │
                │                                    │
                │  /home/user/myapp/                │
                │   ├── index.html                  │
                │   ├── style.css                   │
                │   └── ...                          │
                │       ▲                            │
                │       │ (cùng 1 file system)       │
                │       │                            │
                │  ┌────┴────────────────┐          │
                │  │   Container          │          │
                │  │                       │          │
                │  │  /usr/share/nginx/   │          │
                │  │     html/            │          │
                │  └──────────────────────┘          │
                └────────────────────────────────────┘

                Sửa file trên host → container thy ngay
                Sửa từ container → host thy ngay
                CÙNG file thật, không có copy

Cú pháp

# Cú pháp -v (cũ)
docker run -v /path/on/host:/path/in/container IMAGE

# Cú pháp --mount (mới, rõ ràng hơn)
docker run --mount type=bind,source=/path/on/host,target=/path/in/container IMAGE

Ví dụ thực tế

Gắn folder web vào nginx — sửa code không cần rebuild container:

mkdir -p ~/web && cat > ~/web/index.html <<'EOF'
<h1>Hello from bind mount!</h1>
EOF

docker run -d \
    --name nginx-dev \
    -v ~/web:/usr/share/nginx/html \
    -p 8080:80 \
    nginx

curl http://localhost:8080
# <h1>Hello from bind mount!</h1>

# Sửa file trên host
echo "<h1>Updated!</h1>" > ~/web/index.html

# Container thấy ngay
curl http://localhost:8080
# <h1>Updated!</h1>

→ Không restart container, không rebuild image. Magic cho develop.

Permission issue

⚠️ Bind mount sync 2 chiều → permission có thể không khớp.

Host: user `dev` (UID 1000)
Container: user `nginx` (UID 101)

Container nginx ghi file → file owner UID 101
Host: user dev không đọc/sửa được file đó

Quick fix lab (insecure):

sudo chmod -R 777 /home/user/data

Production: dùng same UID/GID giữa host user và container user, hoặc dùng named volume thay vì bind mount.

Khi nào dùng Bind Mount

  • Development — sửa code, container reflect ngay.
  • Web server — update static file không cần restart.
  • Log files — log container ra ngoài host để dễ đọc/parse.
  • Config files — mount config từ host vào container (nginx.conf, app.config).
  • Backup/restore — copy data từ container ra host.
  • Production database — phụ thuộc filesystem cụ thể, có thể không portable.
  • Permission phức tạp — sync UID/GID giữa host và container.

3. tmpfs — RAM storage

Lưu data vào RAM (bộ nhớ tạm). Cực nhanh nhưng mất khi container die.

                ┌──────────────────────────────────┐
                │       Docker Host                 │
                │                                    │
                │  ┌──────────────────────────┐    │
                │  │       Container          │    │
                │  │                          │    │
                │  │  /tmp/cache              │    │
                │  │     │                    │    │
                │  └─────┼────────────────────┘    │
                │        │                          │
                │        │ (in RAM, không lên disk)│
                │        ▼                          │
                │  ┌──────────────────────────┐    │
                │  │      Host RAM (8GB)      │    │
                │  │   ████████░░░░░░░ 50%    │    │
                │  └──────────────────────────┘    │
                │                                    │
                │  Container die → RAM region freed│
                │                                    │
                └────────────────────────────────────┘
docker run -d \
    --tmpfs /tmp:rw,size=512m \
    --name my-app \
    nginx
# Container có /tmp 512MB trong RAM

Use case:

  • Session cache — data tạm cho 1 phiên, không cần persistent.
  • Hiệu suất cao — RAM nhanh hơn disk 100-1000x.
  • Security — secret/sensitive data không lưu disk.

So sánh 3 phương pháp

Tiêu chí Volume Bind Mount tmpfs
Vị trí /var/lib/docker/volumes/ Bất kỳ đâu trên host RAM
Quản lý Docker Bạn Docker (auto cleanup)
Persistent ✅ Yes ✅ Yes ❌ No
Tốc độ Trung bình Trung bình Rất nhanh
Truy cập từ host Khó (cần Docker path) Trực tiếp Không
Use case DB data Dev code, config Session cache
Portability ✅ Cross-OS ⚠️ Path-dependent

Quick rule

Production database     → Volume
Development code        → Bind Mount
Config files            → Bind Mount (mount nginx.conf, .env)
Session/cache temporary → tmpfs
Backup/restore          → Volume + tar

Decision flowchart — chọn nhanh storage method

                ┌──────────────────────────┐
                │  Có cần persistent       │
                │  (data sống lâu hơn      │
                │  container)?             │
                └────────────┬─────────────┘
                             │
                ┌────────────┴────────────┐
                │                          │
              NO                          YES
                │                          │
                ▼                          ▼
        ┌──────────────┐         ┌────────────────────┐
        │   tmpfs      │         │ Cần sửa file từ     │
        │ (RAM, cache, │         │ host dễ dàng        │
        │  session)    │         │ (dev, log, config)? │
        └──────────────┘         └─────────┬──────────┘
                                            │
                              ┌─────────────┴─────────────┐
                              │                            │
                            YES                           NO
                              │                            │
                              ▼                            ▼
                     ┌─────────────────┐         ┌──────────────────┐
                     │  Bind Mount     │         │     Volume       │
                     │                 │         │                   │
                     │  - dev code     │         │  - production DB  │
                     │  - config files │         │  - app state      │
                     │  - log to host  │         │  - upload folder  │
                     └─────────────────┘         └──────────────────┘

docker cp — copy file giữa host và container

Copy file giữa host và container đang chạy, không cần mount.

# Copy file từ host vào container
docker cp /home/user/data.txt my-container:/app/data.txt

# Copy file từ container ra host
docker cp my-container:/app/output.log /home/user/output.log

# Copy thư mục
docker cp /home/user/data my-container:/app/data

# Wildcard không hỗ trợ — copy folder thay vì

Use case:

  • Quick file transfer cho debug.
  • Copy log ra ngoài để phân tích.
  • Đẩy file vào container đang chạy mà không restart.

⚠️ Đây là ad-hoc command, không phải solution persistent. Production dùng volume.


docker commit — tạo image từ container

Tạo image mới với toàn bộ trạng thái của container đang chạy.

# Run container, sửa đổi gì đó
docker run -it --name my-temp ubuntu bash
# Trong container:
apt update && apt install -y curl vim
exit

# Container còn (đã exit), commit thành image
docker commit my-temp my-custom-ubuntu

# Image mới có sẵn curl + vim
docker run --rm my-custom-ubuntu which curl
# /usr/bin/curl

⚠️ Anti-pattern trong production. Lý do:

  • Image build từ docker commit không reproducible (không có Dockerfile).
  • Khó audit, khó update.
  • Image to (chứa cache, log của container).

Chỉ dùng docker commit cho debug/backup tạm. Production luôn build qua Dockerfile.


Câu chuyện thực tế

Năm 2021, mình làm freelance cho 1 startup nhỏ. Họ deploy app lên 1 server VPS, mọi thứ trong Docker. Sau 6 tháng chạy ngon, 1 ngày sếp gọi panic:

"App chạy vẫn được nhưng tất cả data từ tháng trước biến mất."

SSH vào server kiểm tra. Container database mysql đã restart 2 ngày trước (server reboot). Vào container:

docker exec mysql-db mysql -e "SELECT COUNT(*) FROM users"
# COUNT(*): 0     ← !!!!

Check lại lệnh deploy ban đầu:

docker run -d --name mysql-db \
    -e MYSQL_ROOT_PASSWORD=xxx \
    -p 3306:3306 \
    mysql:8

KHÔNG có -v mount volume. MySQL ghi data vào /var/lib/mysql bên trong container → 6 tháng data nằm trong writable layer.

Khi server reboot → container restart → writable layer mất (Docker tạo writable layer mới).

Họ mất 6 tháng dữ liệu khách hàng vì 1 dòng lệnh thiếu -v.

Fix sau:

docker run -d --name mysql-db \
    -e MYSQL_ROOT_PASSWORD=xxx \
    -p 3306:3306 \
    -v mysql-data:/var/lib/mysql \  # ← Persistent!
    mysql:8

Plus restore từ backup tape (may còn, may quá).

Bài học:

  1. MỌI container có state PHẢI có volume. DB, redis, app upload folder — không có ngoại lệ.

  2. Test restart container trước production. Stop + remove + recreate → verify data còn nguyên. Đây là smoke test cơ bản.

  3. Backup volume định kỳ. Tar volume ra disk, copy ra S3. Test restore quý/tháng.

  4. docker run không có -v = data ephemeral. Recheck mọi lệnh deploy DB trước go live.

  5. Đọc docs official. MySQL image mysql:8 Dockerfile có VOLUME ["/var/lib/mysql"] — nhưng nếu user không mount volume, Docker tạo anonymous volume tự xóa khi container bị --rm. Hint không đủ — phải explicit -v.

  6. Server reboot là test thực tế. Reboot host = restart Docker daemon = mọi container recreate. Đây là moment of truth.


Lab đầy đủ

Lab 1: Custom Bridge Network + DNS

# Tạo network
docker network create app-net

# Run web và db cùng network
docker run -d --name db --network app-net mysql:8 \
    -e MYSQL_ROOT_PASSWORD=test
docker run -d --name web --network app-net nginx

# Test DNS
docker exec web ping db -c 2
# 64 bytes from db (172.18.0.2)   ← DNS work

# Cleanup
docker rm -f web db
docker network rm app-net

Lab 2: Load balancing qua --network-alias

docker network create lb-net

# 3 container web với cùng alias
for i in 1 2 3; do
    docker run -d --name web-$i --network lb-net \
        --network-alias web nginx
    docker exec web-$i sh -c "echo 'Server $i' > /usr/share/nginx/html/index.html"
done

# Test load balancing
docker run --rm --network lb-net alpine sh -c "
for i in 1 2 3 4 5; do
    wget -qO- web | head -1
done
"
# Server 1
# Server 2
# Server 3
# Server 1
# Server 2

docker rm -f web-1 web-2 web-3
docker network rm lb-net

Lab 3: Volume — Database persistent

# Tạo volume
docker volume create db-data

# Run MySQL với volume
docker run -d --name mydb \
    -e MYSQL_ROOT_PASSWORD=secret \
    -v db-data:/var/lib/mysql \
    mysql:8

sleep 30  # đợi MySQL init

# Tạo database + table + data
docker exec -i mydb mysql -psecret <<EOF
CREATE DATABASE shop;
USE shop;
CREATE TABLE products (id INT, name VARCHAR(50));
INSERT INTO products VALUES (1, 'Apple'), (2, 'Banana');
EOF

# XÓA container hoàn toàn
docker rm -f mydb

# Tạo container mới với CÙNG volume
docker run -d --name mydb-new \
    -e MYSQL_ROOT_PASSWORD=secret \
    -v db-data:/var/lib/mysql \
    mysql:8

sleep 30

# Verify data còn nguyên
docker exec mydb-new mysql -psecret -e "SELECT * FROM shop.products"
# +----+--------+
# | id | name   |
# +----+--------+
# |  1 | Apple  |
# |  2 | Banana |
# +----+--------+

# Cleanup
docker rm -f mydb-new
docker volume rm db-data

Lab 4: Bind Mount — Development workflow

mkdir ~/myweb && echo "<h1>Version 1</h1>" > ~/myweb/index.html

docker run -d --name dev-web \
    -v ~/myweb:/usr/share/nginx/html \
    -p 8080:80 \
    nginx

curl http://localhost:8080
# <h1>Version 1</h1>

# Sửa file trên host
echo "<h1>Version 2 - no restart!</h1>" > ~/myweb/index.html

curl http://localhost:8080
# <h1>Version 2 - no restart!</h1>     ← Container reflect ngay!

docker rm -f dev-web

Lab 5: Volume backup + restore

# Tạo volume có data
docker volume create source-vol
docker run --rm -v source-vol:/data alpine sh -c "
echo 'Important data' > /data/file1.txt
echo 'More data' > /data/file2.txt
"

# Backup volume
docker run --rm \
    -v source-vol:/data \
    -v $(pwd):/backup \
    alpine \
    tar czf /backup/backup.tar.gz -C /data .

ls -lh backup.tar.gz
# -rw-r--r-- 1 root root 200 ...  backup.tar.gz

# Restore vào volume mới
docker volume create restored-vol
docker run --rm \
    -v restored-vol:/data \
    -v $(pwd):/backup \
    alpine \
    tar xzf /backup/backup.tar.gz -C /data

# Verify
docker run --rm -v restored-vol:/data alpine ls /data
# file1.txt
# file2.txt

# Cleanup
docker volume rm source-vol restored-vol
rm backup.tar.gz

Lab 6: tmpfs trong RAM

docker run -d --name cache-app \
    --tmpfs /tmp:rw,size=100m \
    nginx

# Ghi file vào tmpfs
docker exec cache-app sh -c "
dd if=/dev/zero of=/tmp/cache bs=1M count=50
df -h /tmp
"
# Filesystem      Size  Used  Avail Use%
# tmpfs           100M  50M   50M   50%

# Stop + start container → tmpfs reset
docker restart cache-app
docker exec cache-app df -h /tmp
# tmpfs           100M  0     100M  0%    ← Reset!

docker rm -f cache-app

Pitfalls

  1. Default bridge — DNS không work → container không thấy nhau bằng tên. Luôn tạo custom bridge cho app.

  2. Run DB không có -v volume → restart = mất data. Catastrophic cho production.

  3. Bind mount permission sai → container không ghi được file. Quick fix chmod 777 (insecure), tốt hơn: align UID/GID.

  4. Host network conflict — 2 container cùng dùng --network host listen cùng port → port busy fail.

  5. docker cp thay vì volume → mọi lần update phải cp lại. Anti-pattern, dùng bind mount/volume.

  6. docker commit cho production → image không reproducible, khó audit. Chỉ dùng debug.

  7. Volume name trùng giữa 2 stack → 2 app share data ngẫu nhiên. Đặt tên rõ ràng (projectA_db_data).

  8. Bind mount path tuyệt đối → script portable kém. Dùng $(pwd) hoặc env var.

  9. Quên docker volume prune định kỳ → volume orphan tích tụ, ngốn disk. Cleanup tháng/tuần.

  10. macvlan trong VPN tunnel → có thể không work do MTU/encapsulation. Test trước.

  11. Overlay không bật ingress firewall → Swarm port (2377, 7946, 4789) bị chặn → cluster fail.

  12. --tmpfs size không set → mặc định no limit, có thể OOM host. Luôn size=Nm.

  13. Bind mount vào critical path (/etc, /lib) → container thấy file host, security risk. Mount đúng folder cần.

  14. DB volume mount path sai — MySQL /var/lib/mysql, PostgreSQL /var/lib/postgresql/data, Mongo /data/db. Check docs image.


Tóm tắt

Network

  • 5 network drivers: bridge (default), host, none, overlay (Swarm), macvlan.
  • Bridge default: container có IP riêng, NAT ra ngoài, cần -p cho inbound. DNS không work giữa các container.
  • Custom bridge: DNS WORK giữa container qua --name. Production luôn dùng custom bridge.
  • --network-alias cho phép nhiều container share 1 tên → load balancing built-in qua DNS round-robin.
  • Host network: zero overhead, dễ conflict port.
  • None: offline batch, security extreme.
  • Overlay: multi-host (Swarm/K8s).
  • Macvlan: container có IP thật trên LAN.
  • 1 container nhiều network: dùng docker network connect thêm network → microservices pattern (web cầu nối frontend ↔ backend).
  • Immutable Infrastructure: không sửa container, deploy lại với image mới. Container là "cattle, not pet".

Storage

  • Container ephemeral — writable layer mất khi container rm.
  • 3 phương pháp persistent:
    • Volume — Docker managed, dùng cho production DB/state.
    • Bind Mount — host folder, dùng cho dev code/config.
    • tmpfs — RAM, cache tạm cực nhanh, không persistent.
  • -v name:/path hoặc --mount type=volume,source=name,target=/path.
  • Backup volume: dùng container alpine + tar.
  • docker cp — ad-hoc copy, không thay volume.
  • docker commit — anti-pattern production, chỉ debug.
  • Quy tắc vàng: mọi container có state PHẢI có volume.

Bài tiếp: 04 — Docker Compose (Capstone). Dựng full LEMP stack qua 1 file YAML, 1 lệnh docker compose up.

← Bài trước
Bài 02
Bài tiếp theo →
Bài 04
Zalo tư vấn