Volume và network: order-service gọi Postgres
Tiếp đi: cho order-service nói chuyện với Postgres chạy trong container.
Phần này có hai bài toán dính nhau: data của Postgres sống ở đâu (volume) và hai container gọi nhau thế nào (network).
1. Bài toán
┌─────────────────────┐ ┌─────────────────────┐
│ Container A │ JDBC │ Container B │
│ order-service :8080 │ ───────► │ PostgreSQL :5432 │
└─────────────────────┘ │ bảng orders │
└─────────────────────┘
Cách làm đầu tiên ai cũng thử:
spring.datasource.url=jdbc:postgresql://localhost:5432/orders
2. Vấn đề
Vấn đề 1: localhost sai chỗ. Mỗi container có network namespace riêng:
Container A: localhost → chính Container A
Container B: localhost → chính Container B
Windows host: localhost → chính Windows
localhost luôn là "môi trường network của process hiện tại". order-service chạy trong A nên localhost:5432 là A:5432, không có gì ở đó → Connection refused.
Vấn đề 2: data mất theo container. Postgres ghi file vào writable layer. docker rm container là mất sạch bảng orders.
3. Giải pháp
Volume: đưa data ra khỏi lifecycle của container.
Postgres Container
/var/lib/postgresql/data
│ mount
▼
pg_data (Docker Volume) ← sống lâu hơn container
Có hai kiểu mount hay dùng:
- Named volume (
-v pg_data:/var/lib/postgresql/data): Docker quản lý vị trí lưu. Dùng cho data DB. - Bind mount (
-v ./init.sql:/docker-entrypoint-initdb.d/init.sql): mount file/folder cụ thể của host. Dùng cho config, script init, source khi dev.
User-defined bridge network: các container cùng network gọi nhau bằng tên container; Docker có DNS nội bộ (127.0.0.11) resolve tên → IP.
order-service: "postgres-db là ai?"
│
▼
Docker DNS → 172.18.0.2 → Postgres container
Lưu ý: network bridge mặc định của Docker không có DNS theo tên. Phải tự tạo network (Compose tự làm việc này).
4. Code từng bước
Bước 1: Tạo network và volume
docker network create order-net
docker volume create pg_data
Bước 2: Chạy Postgres và Redis. Password lấy từ biến môi trường của shell, không ghi thẳng vào lệnh:
export POSTGRES_PASSWORD=... # đặt trong shell/.env local, không commit
docker run -d \
--name postgres-db \
--network order-net \
-e POSTGRES_DB=orders \
-e POSTGRES_USER=order_app \
-e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
-v pg_data:/var/lib/postgresql/data \
postgres:17
docker run -d --name redis --network order-net redis:7
Không có -p: Postgres và Redis chỉ cần được gọi từ trong network.
Bước 3: application.properties đọc từ env
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/orders}
spring.datasource.username=${DB_USERNAME:order_app}
spring.datasource.password=${DB_PASSWORD}
spring.data.redis.host=${REDIS_HOST:localhost}
spring.data.redis.port=6379
${DB_URL:default}: có biến DB_URL thì dùng, không có thì về localhost. Chạy Java trên máy dùng localhost, chạy trong Docker dùng tên container. Cùng một jar, không sửa code.
Bước 4: Chạy order-service và test
docker run -d \
--name order-service \
--network order-net \
-p 8080:8080 \
-e DB_URL=jdbc:postgresql://postgres-db:5432/orders \
-e DB_PASSWORD="$POSTGRES_PASSWORD" \
-e REDIS_HOST=redis \
order-service:1.0
curl -X POST localhost:8080/orders -H "Content-Type: application/json" \
-d '{"productId":"P-1","quantity":2}'
curl localhost:8080/orders
# thử DNS từ trong container
docker exec -it order-service getent hosts postgres-db
Thử volume: xóa Postgres rồi tạo lại với cùng volume, data vẫn còn.
docker rm -f postgres-db
docker run -d --name postgres-db --network order-net \
-e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
-v pg_data:/var/lib/postgresql/data postgres:17
curl localhost:8080/orders # đơn hàng cũ vẫn còn
5. Cách hoạt động
Browser
│ localhost:8080
▼
Windows Host
│ -p 8080:8080
▼
┌──────────────── Docker network: order-net ────────────────┐
│ │
│ ┌──────────────────┐ postgres-db:5432 ┌──────────────┐ │
│ │ order-service │ ────────────────► │ postgres-db │──┼──► pg_data (volume)
│ │ Tomcat :8080 │ └──────────────┘ │
│ │ │ redis:6379 ┌──────────────┐ │
│ │ │ ────────────────► │ redis │ │
│ └──────────────────┘ └──────────────┘ │
└───────────────────────────────────────────────────────────┘
Container → Container : Docker network, tên container (postgres-db:5432)
Host → Container : Port mapping -p (localhost:5432)
6. So sánh
| Named volume | Bind mount | Writable layer | |
|---|---|---|---|
| Ai quản lý vị trí | Docker | Bạn (path host) | Docker, gắn với container |
Sống sau docker rm | ✅ | ✅ | ❌ |
| Dùng cho | Data DB, Redis AOF | Config, script init, hot reload khi dev | File tạm |
| Hiệu năng trên Docker Desktop | Tốt | Chậm hơn (đồng bộ file Windows/Mac) | Tốt |
| Network | Đặc điểm |
|---|---|
bridge mặc định | Có IP nội bộ, không có DNS theo tên |
| User-defined bridge | DNS theo tên container, cô lập theo network |
host | Dùng chung network với host (Linux), không cần -p, mất cô lập |
none | Không có network |
7. Khi nào nên dùng / không nên dùng
- Nên: named volume cho mọi DB chạy trong container; user-defined network cho các container cần gọi nhau.
- Nên: chỉ
-pnhững port cần truy cập từ host. Muốn dùng DBeaver/pgAdmin thì map-p 127.0.0.1:5432:5432(chỉ bind localhost). - Không nên: hard-code IP container (IP đổi khi tạo lại); dùng
localhostđể gọi container khác. - Production: DB thường dùng dịch vụ managed (Azure Database for PostgreSQL, RDS) thay vì container tự quản; volume trên một máy không tự backup hay replicate.
Nếu Postgres cài thẳng trên Windows, không chạy bằng Docker, thì order-service trong container gọi localhost:5432 được không?
Không được, cùng lý do ở trên: localhost trong container là chính container. Bạn cần địa chỉ của host nhìn từ trong container:
# Docker Desktop (Windows/Mac): có sẵn tên này
DB_URL=jdbc:postgresql://host.docker.internal:5432/orders
# Linux: tự thêm mapping
docker run --add-host=host.docker.internal:host-gateway ... order-service:1.0
order-service container
│ host.docker.internal:5432
▼
Windows host :5432 → PostgreSQL cài trên Windows
Thêm một bẫy: Postgres trên host phải listen trên interface mà container tới được (không chỉ 127.0.0.1 nếu là Linux), và pg_hba.conf phải cho phép dải IP của Docker network.
-p 5432:5432 cho Postgres có cần không?Vậy Postgres có cần -p 5432:5432 không? Mình thấy nhiều tutorial luôn có dòng đó.
Không cần cho order-service. Hai container nói chuyện trong network nội bộ, không đi qua host. Tutorial thêm -p là để tool trên máy bạn (DBeaver, IntelliJ, pgAdmin) connect vào:
DBeaver → localhost:5432 → Windows :5432 → (port mapping) → postgres container :5432
Ở production thì càng không nên publish port DB ra host: -p 5432:5432 mặc định bind 0.0.0.0, tức mọi interface, và Docker tự thêm rule iptables nên có thể vượt qua firewall (ufw) của host. Nếu buộc phải map, dùng -p 127.0.0.1:5432:5432.
Phỏng vấn hỏi Docker volume và networking thì trả lời sao?
Q1. Làm sao giữ data của database khi container bị xóa?
🇻🇳 Tiếng Việt:
Data trong writable layer mất theo container, nên em mount named volume vào thư mục data của DB, ví dụ
/var/lib/postgresql/data. Volume có lifecycle riêng: xóa và tạo lại container vẫn đọc được data cũ. Bind mount thì em dùng cho file config hoặc script init.
🇬🇧 English:
Data in the container's writable layer is lost when the container is removed, so I mount a named volume at the database data directory, for example
/var/lib/postgresql/data. The volume has its own lifecycle, so a recreated container still sees the old data. I use bind mounts for config files or init scripts.
Q2. Hai container gọi nhau thế nào? Vì sao localhost không chạy?
🇻🇳 Tiếng Việt:
Mỗi container có network namespace riêng, nên
localhostchỉ trỏ về chính nó. Em đặt các container vào cùng user-defined bridge network, Docker có DNS nội bộ resolve tên container hoặc tên service thành IP, ví dụpostgres-db:5432.
🇬🇧 English:
Each container has its own network namespace, so
localhostrefers to the container itself. I put the containers on the same user-defined bridge network, where Docker's embedded DNS resolves container or service names to IPs, for examplepostgres-db:5432.
Q3. -p 8080:8080 làm gì?
🇻🇳 Tiếng Việt:
Nó publish port: map port 8080 của host vào port 8080 của container, cú pháp
host:container. Chỉ cần cho traffic từ ngoài vào container. Giao tiếp giữa các container trong cùng network không cần-p, và em tránh publish port DB ra ngoài.
🇬🇧 English:
It publishes a port by mapping host port 8080 to container port 8080, using the
host:containersyntax. It is only needed for traffic coming from outside. Containers on the same network talk to each other without-p, and I avoid publishing database ports.
Cách nhớ ngắn gọn
- localhost trong container = chính container.
- Container → container: cùng network, gọi bằng tên.
- Host → container:
-p host:container. - Data DB: named volume. Config: bind mount.
- Container → host:
host.docker.internal.
Tự kiểm tra
jdbc:postgresql://localhost:5432 trong container order-service trỏ đi đâu?
Về chính container order-service, không phải Postgres hay host.
Vì sao network bridge mặc định không gọi được bằng tên container?
Bridge mặc định không có DNS theo tên; chỉ user-defined network (và Compose) mới có DNS nội bộ.
docker rm -f postgres-db rồi tạo lại với cùng -v pg_data:... thì data sao?
Vẫn còn, vì data nằm trong volume pg_data, không nằm trong container.
Khi nào cần -p 5432:5432 cho Postgres?
Chỉ khi tool trên host cần connect vào. Container khác trong cùng network không cần.
Container muốn gọi service chạy trên Windows host thì dùng địa chỉ gì?
host.docker.internal (Docker Desktop); trên Linux thêm --add-host=host.docker.internal:host-gateway.