Image, layer, container: đóng gói order-service
Mình muốn học Docker, Kubernetes và Helm.
Được. Học theo đúng thứ tự, đừng học song song:
Ứng dụng Spring Boot
↓
1. Docker "Đóng gói app thành container"
↓
2. Kubernetes "Chạy và quản lý nhiều container"
↓
3. Helm "Đóng gói và quản lý Kubernetes YAML"
Chương này là Docker. Ta dùng một project chạy xuyên suốt thay vì ví dụ nginx rời rạc: order-service (Spring Boot 3, Java 17) dùng PostgreSQL và Redis. Cuối chương, cả ba chạy bằng một lệnh docker compose up.
1. Bài toán: "works on my machine"
Bạn có order-service.jar. Trên laptop chạy ngon:
java -jar target/order-service.jar
Đưa sang server khác thì phải lo:
Server
├── Java đã cài chưa?
├── Java version đúng chưa? (17 hay 11?)
├── config đúng chưa?
├── timezone, locale, font, thư viện OS?
└── java -jar order-service.jar
2. Vấn đề
Laptop của bạn Server
────────────── ──────
Java 17 Java ???
Spring Boot ──────► Config ???
order-service.jar Environment ???
- Môi trường mỗi máy một kiểu, lỗi chỉ xuất hiện trên server.
- Cài Java, cấu hình OS bằng tay, khó lặp lại.
- Muốn chạy 3 bản giống hệt nhau thì phải dựng 3 môi trường giống hệt nhau.
3. Giải pháp: Image và Container
Docker đóng gói cả môi trường chạy app thành một Image:
Docker Image: order-service:1.0
┌──────────────────────┐
│ Linux userland │
│ Java 17 (JRE) │
│ order-service.jar │
│ startup command │
└──────────────────────┘
Từ Image, ta tạo Container:
IMAGE
│
docker run
│
┌─────┼─────┐
▼ ▼ ▼
app-1 app-2 app-3
- Image = template/package, read-only.
- Container = một instance đang chạy từ Image.
Giống tư duy Java (không giống hoàn toàn về kỹ thuật, nhưng dễ nhớ):
Java Docker
Class Image
│ new │ docker run
▼ ▼
Object Container
Layer. Image không phải một file khổng lồ. Nó là chồng các layer read-only, mỗi lệnh trong Dockerfile (FROM, COPY, RUN...) tạo một layer. Container thêm một writable layer mỏng ở trên cùng:
┌─────────────────────────────┐
│ writable layer (container) │ ← mất khi docker rm
├─────────────────────────────┤
│ COPY order-service.jar │ layer 3 (đổi mỗi lần build)
├─────────────────────────────┤
│ WORKDIR /app │ layer 2
├─────────────────────────────┤
│ eclipse-temurin:17-jre │ layer 1 (base image, dùng chung)
└─────────────────────────────┘
- Nhiều image dùng chung base layer: tải và lưu một lần.
- Build lại: layer nào không đổi thì dùng build cache, chỉ build lại từ layer thay đổi trở xuống.
- Vì vậy thứ ít đổi (base, dependency) đặt trên, thứ hay đổi (code, jar) đặt dưới.
Registry. Nơi lưu và phát hành image: Docker Hub, GitHub Container Registry, Azure Container Registry, ECR... Server chỉ cần docker pull là có đúng image bạn đã build.
Developer ──docker push──► Registry ──docker pull──► Server / Kubernetes
4. Code từng bước
Bước 1: Chạy thử image có sẵn
docker pull nginx
docker run nginx
Image nginx được tải về, Docker tạo một container từ image đó.
Bước 2: Dockerfile tối thiểu cho order-service (chi tiết ở bài 16.2)
order-service/
├── src/
├── pom.xml
├── target/
│ └── order-service.jar
└── Dockerfile
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/order-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Bước 3: Build image
mvn clean package
docker build -t order-service:1.0 .
docker images
REPOSITORY TAG
order-service 1.0
Lúc này chỉ có IMAGE, chưa có app nào đang chạy.
Bước 4: Chạy container và các lệnh cơ bản
# chạy nền (-d), đặt tên, map port host 8080 → container 8080
docker run -d --name order-1 -p 8080:8080 order-service:1.0
docker ps # container đang chạy
docker ps -a # cả container đã stop
docker logs order-1 # xem log
docker logs -f order-1 # theo dõi log realtime
docker exec -it order-1 sh # chạy thêm process sh bên trong container
docker stop order-1 # RUNNING → STOPPED, container vẫn còn
docker start order-1 # STOPPED → RUNNING
docker rm order-1 # xóa container, image vẫn còn
curl http://localhost:8080/actuator/health
Chạy nhiều container từ cùng một image:
docker run -d -p 8081:8080 order-service:1.0
docker run -d -p 8082:8080 order-service:1.0
docker run -d -p 8083:8080 order-service:1.0
5. Cách hoạt động
Browser
│ GET localhost:8080/orders/1
▼
Host port 8080
│ Docker port mapping (-p 8080:8080)
▼
┌───────────────── Container ─────────────────┐
│ java -jar app.jar (main process) │
│ └── Spring Boot │
│ └── Embedded Tomcat :8080 │
│ └── DispatcherServlet │
│ └── OrderController │
└─────────────────────────────────────────────┘
Docker không thay thế Tomcat. Docker chỉ tạo môi trường chứa process Java đó. Container sống khi main process còn sống: Java chết thì container stop.
6. So sánh
| Image | Container | |
|---|---|---|
| Bản chất | Template read-only gồm các layer | Instance đang chạy + writable layer |
| Tạo bằng | docker build / docker pull | docker run |
| Số lượng | Một image | Nhiều container từ cùng image |
| Xóa | docker rmi | docker rm (image không ảnh hưởng) |
| Lưu ở | Local + Registry | Chỉ trên máy đang chạy |
7. Khi nào nên dùng / không nên dùng
- Nên: service backend cần deploy lặp lại, chạy nhiều bản, CI/CD, dựng môi trường local (Postgres, Redis) nhanh.
- Nên: khi sắp lên Kubernetes, vì K8s chạy container image.
- Không cần: script chạy một lần, tool desktop có GUI.
- Cẩn thận: coi container như server lâu dài, SSH vào sửa tay. Container hỏng thì xóa và tạo lại từ image.
docker build -t order-service:1.0 ., cái -t và dấu . cuối là gì?Trong docker build -t, -t có ý nghĩa gì?
-t = tag: đặt tên và version cho image.
docker build -t order-service:1.0 .
│
├── -t order-service:1.0
│ │ │
│ │ └── tag/version = 1.0
│ └──────────────── image name = order-service
│
└── . = build context (folder hiện tại)
Không ghi tag thì Docker hiểu là latest:
docker build -t order-service . # = order-service:latest
docker run order-service # = docker run order-service:latest
Một image có thể có nhiều tag, và tag là thứ ta dùng khi deploy:
order-service:1.2 ← version cũ
order-service:1.3 ← version mới
Kubernetes: image: order-service:1.3
Khi push lên registry, tên đầy đủ có cả host registry:
docker tag order-service:1.3 myregistry.azurecr.io/order-service:1.3
docker push myregistry.azurecr.io/order-service:1.3
Lưu ý production: đừng deploy bằng latest. latest chỉ là một cái tên, không có nghĩa "mới nhất", và nó có thể bị ghi đè. Dùng tag cố định (version hoặc git SHA) để biết chính xác đang chạy bản nào và rollback được.
Còn dấu . là build context: folder mà Docker được phép đọc file khi COPY. Phần này nói kỹ ở bài sau.
Vậy mình docker rm container thì image có mất không? Data trong container thì sao?
Bạn hiểu gần đúng, chỉ cần tách rõ ba thứ:
IMAGE order-service:1.0 ✅ vẫn còn
│
├── old Container A ❌ đã docker rm
└── new Container B ✅ docker run lại
- Xóa container không làm mất image, giống xóa object không làm mất class.
- Nhưng file mà container ghi vào writable layer thì mất theo container.
Với order-service thì không sao vì app stateless. Nhưng nếu Postgres chạy trong container mà không có volume, docker rm là mất sạch bảng orders. Đó là lý do có Volume (bài 16.4).
Mindset quan trọng trước khi sang K8s: container là thứ tạm thời, thay thế được. Hỏng thì xóa, tạo lại từ image.
Phỏng vấn hỏi Docker image, container, layer thì trả lời sao?
Q1. Image khác container thế nào?
🇻🇳 Tiếng Việt:
Image là template read-only, chứa ứng dụng và runtime dependency, ví dụ JRE 17 và file jar. Container là một instance đang chạy từ image, có thêm một writable layer riêng. Từ một image em chạy được nhiều container. Xóa container không ảnh hưởng image.
🇬🇧 English:
A Docker image is a read-only template that contains the application and its runtime dependencies, for example a JRE and the JAR file. A container is a running instance of that image with its own thin writable layer. One image can start many containers, and removing a container does not affect the image.
Q2. Layer và build cache là gì?
🇻🇳 Tiếng Việt:
Mỗi lệnh trong Dockerfile tạo một layer read-only. Layer được dùng chung giữa các image và được cache khi build. Nếu một layer đổi thì nó và mọi layer phía sau phải build lại, nên em đặt phần ít đổi như base image và dependency lên trước, source code xuống sau.
🇬🇧 English:
Each Dockerfile instruction creates a read-only layer. Layers are shared between images and cached during builds. When a layer changes, it and all following layers are rebuilt, so I put rarely changing parts like the base image and dependencies first and the source code last.
Q3. Tag và registry dùng thế nào khi deploy?
🇻🇳 Tiếng Việt:
CI build image, gắn tag cố định như version hoặc git SHA, rồi push lên registry như ACR hoặc ECR. Server hoặc Kubernetes pull đúng tag đó. Em tránh dùng
latestở production vì nó có thể bị ghi đè, khó biết đang chạy bản nào và khó rollback.
🇬🇧 English:
The CI pipeline builds the image, tags it with an immutable tag such as the version or git SHA, and pushes it to a registry like ACR or ECR. Servers or Kubernetes then pull exactly that tag. I avoid
latestin production because it can be overwritten, which makes it hard to know what is running and to roll back.
Cách nhớ ngắn gọn
- Image : Container = Class : Object.
- Image = chồng layer read-only; container = + writable layer.
- Ít đổi lên trên, hay đổi xuống dưới để tận dụng cache.
-t name:tag; production dùng tag cố định, không dùnglatest.- Xóa container mất writable layer, không mất image.
Tự kiểm tra
"Works on my machine" được Docker giải quyết thế nào?
Đóng gói cả runtime (JRE, thư viện OS) cùng app vào image, nên máy nào chạy image đó cũng có cùng môi trường.
docker build -t order-service . tạo image có tag gì?
order-service:latest, vì không ghi tag thì mặc định là latest.
Sửa một dòng code rồi build lại, layer nào được cache?
Các layer phía trên lệnh COPY source/jar (base image, WORKDIR, dependency) được cache; từ layer COPY thay đổi trở xuống phải build lại.
docker stop và docker rm khác nhau thế nào?
stop chuyển container sang STOPPED, vẫn start lại được. rm xóa container và writable layer của nó.
docker exec -it order-1 sh có phải SSH không?
Không. Docker chạy thêm process sh bên trong container đang chạy. Thoát shell thì process chính (java) vẫn chạy.