Volume và network: order-service gọi Postgres

4 câu hỏi~30 phútTừ khóa: named volume, bind mount, writable layer, bridge network, container DNS, -p host:container, localhost trong container, host.docker.internal

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:

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 volumeBind mountWritable layer
Ai quản lý vị tríDockerBạn (path host)Docker, gắn với container
Sống sau docker rm✅✅❌
Dùng choData DB, Redis AOFConfig, script init, hot reload khi devFile tạm
Hiệu năng trên Docker DesktopTốtChậm hơn (đồng bộ file Windows/Mac)Tốt
NetworkĐặc điểm
bridge mặc địnhCó IP nội bộ, không có DNS theo tên
User-defined bridgeDNS theo tên container, cô lập theo network
hostDùng chung network với host (Linux), không cần -p, mất cô lập
noneKhông có network

7. Khi nào nên dùng / không nên dùng

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.

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 localhost chỉ 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 localhost refers 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 example postgres-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:container syntax. 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


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.