1. Container vs VM — khác biệt cốt lõi
Trước Docker (2013), 2 cách phổ biến deploy app:
- Bare metal — install app trực tiếp lên server. Conflict dependency cao.
- VM (Virtual Machine) — VMware/KVM/Hyper-V. Mỗi app 1 VM với full OS. Isolation tốt nhưng nặng (mỗi VM 500MB+ RAM, vài GB disk, boot 30s).
Container giải bài toán: isolation tương tự VM nhưng nhẹ hơn 100×:
1.1. So sánh chi tiết
| Tiêu chí | VM | Container |
|---|---|---|
| Kernel | Riêng (full OS) | Chia sẻ với host |
| Boot time | 30s - vài phút | < 1s |
| Size | vài GB | vài MB - vài trăm MB |
| Density | 10-20 VM/host | 100-1000+ container/host |
| Isolation | Hardware-level (mạnh) | OS-level (process) |
| Image format | VMDK, QCOW2 (vài GB) | OCI image (vài MB+) |
| OS hỗ trợ | Bất kỳ OS | Cùng kernel với host (Linux container chỉ chạy trên Linux host) |
1.2. Khi nào VM tốt hơn?
- Need full OS isolation (multi-tenant cloud).
- Cross-OS (Windows app trên Linux host).
- Hardware passthrough (GPU, custom kernel).
Trong DevOps modern: VM là infrastructure layer; container là application layer. Cloud thường chạy K8s trong VM (EKS node = EC2 instance) — combo tốt nhất.
2. Cách container hoạt động — Linux primitives
Container không phải magic. Nó là 3 Linux features cũ kết hợp:
2.1. Namespaces — isolation
Namespace tạo "view" riêng của system resource cho process:
| Namespace | Isolate |
|---|---|
| PID | Process IDs (process trong container thấy mình PID 1) |
| Network (NET) | Interface, route, port |
| Mount (MNT) | Filesystem mount points |
| UTS | Hostname, domain name |
| IPC | Inter-process communication (shared memory, queues) |
| User (USER) | UIDs, GIDs (UID 0 trong container = UID 1000 ngoài) |
| Cgroup | Cgroup root (Linux 4.6+) |
2.2. Cgroups — resource limits
Control Groups (cgroups) giới hạn tài nguyên process:
- cpu — share CPU time, set quota.
- memory — limit RAM, kill nếu vượt (OOM kill).
- blkio — limit disk I/O.
- pids — limit số process.
- net_cls — tag traffic cho QoS.
2.3. Capabilities + seccomp — security
Linux capability chia quyền root thành 40+ phần (CAP_NET_ADMIN, CAP_SYS_ADMIN, ...). Container chỉ giữ lại capability cần thiết.
seccomp filter syscall — block syscall nguy hiểm.
2.4. Demo container "thủ công" với unshare
# Bash + unshare = container thô sơ
sudo unshare --pid --net --mount --uts --ipc --fork bash
# Trong shell mới:
hostname container1 # đổi hostname (UTS namespace)
mount -t proc proc /proc # remount proc
ps aux # chỉ thấy process trong namespace
ip addr # chỉ thấy lo (network namespace)
exit
Đó là "container" — không cần Docker. Docker tự động hóa: tạo namespace, mount image filesystem, set cgroup, drop capability.
3. Docker Architecture
Docker không phải 1 binary mà là client-server architecture:
3.1. Components
- Docker CLI —
dockercommand. Gọi REST API. - Docker Daemon (dockerd) — service chạy nền, quản lý high-level objects.
- containerd — high-level runtime (lifecycle container, image transfer).
- runc — low-level OCI runtime tạo namespace+cgroup. Chuẩn OCI.
Modern alternative: Podman — daemonless (mỗi command spawn runc trực tiếp), rootless mặc định, CLI compat Docker.
3.2. OCI Standard
Open Container Initiative (2015) định nghĩa:
- Image Spec — format image (layers, manifest).
- Runtime Spec — cách chạy container.
- Distribution Spec — cách push/pull registry.
Vì OCI: Docker image chạy được với Podman, BuildKit, K8s (containerd, CRI-O). Không lock-in vendor.
4. Container đầu tiên — lệnh cơ bản
# Pull image từ Docker Hub
docker pull nginx:1.25-alpine
# Run container
docker run --rm -d -p 8080:80 --name web nginx:1.25-alpine
# --rm: xóa container khi stop
# -d: detached (background)
# -p host:container — port mapping
# --name: tên container
# Verify
curl http://localhost:8080
docker ps # container đang chạy
# Logs
docker logs web
docker logs -f web # follow
# Exec vào container
docker exec -it web sh
# Trong container:
ps aux
ls /etc/nginx
exit
# Stop & remove
docker stop web
docker rm web # nếu không có --rm
# Liệt kê
docker ps # running
docker ps -a # all (kể cả stopped)
docker images # images
docker volume ls
docker network ls
4.1. Run options quan trọng
docker run \
-d \ # background
--name myapp \ # tên
-p 3000:3000 \ # port
-e NODE_ENV=production \ # env var
--env-file .env \ # env từ file
-v $(pwd)/data:/app/data \ # bind mount
--memory=512m \ # RAM limit
--cpus=0.5 \ # 50% 1 core
--restart=unless-stopped \ # auto restart
--user 1000:1000 \ # non-root
--read-only \ # filesystem read-only
--cap-drop=ALL \ # drop tất cả capability
--cap-add=NET_BIND_SERVICE \ # chỉ giữ cái cần
myapp:1.0
4.2. docker prune — dọn dẹp
docker container prune # xóa container stopped
docker image prune # xóa dangling image
docker image prune -a # xóa unused image
docker volume prune # xóa volume không reference
docker network prune
docker system prune -a --volumes # xóa TẤT CẢ unused (nuclear)
Mẹo: kiểm tra docker system df xem disk usage trước khi prune.
5. Dockerfile — Build image declarative
5.1. Hello world
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
docker build -t myapp:1.0 .
docker run -p 3000:3000 myapp:1.0
5.2. Instruction reference
| Instruction | Mục đích |
|---|---|
FROM | Base image (bắt buộc, dòng đầu) |
WORKDIR | Set working directory (cd) |
COPY src dst | Copy file vào image (preferred) |
ADD | Như COPY nhưng auto-extract tar, fetch URL (avoid) |
RUN cmd | Chạy command tại build time |
ENV KEY=value | Set env var (cả build + runtime) |
ARG name | Build-time variable (không persist runtime) |
EXPOSE port | Document port (chỉ doc, không thực sự open) |
VOLUME /path | Mount point declaration |
USER name | Set user chạy command sau |
HEALTHCHECK | Define health check command |
ENTRYPOINT | Executable mặc định (khó override) |
CMD | Default args (override bằng docker run … cmd) |
5.3. Layer system
Mỗi instruction tạo 1 layer (filesystem diff). Image = stack of layers, immutable, cached.
5.4. Cache mechanism
Khi build lại, Docker check từng instruction:
- Instruction giống và input giống → cache hit, skip.
- Bất kỳ thay đổi → invalidate cache từ đó về sau.
Quy tắc vàng: đặt instruction ít thay đổi ở đầu, nhiều thay đổi ở cuối.
# ❌ KÉM — COPY trước npm ci
FROM node:20-alpine
WORKDIR /app
COPY . . # mỗi lần code đổi → invalidate
RUN npm ci # → rebuild cả npm ci
# ✓ TỐT — separate package.json copy
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./ # ít thay đổi
RUN npm ci # cache khi package.json không đổi
COPY . . # nhiều thay đổi, đặt cuối
5.5. .dockerignore
# .dockerignore — file/folder không vào build context
node_modules
.git
.env
*.log
dist
build
coverage
.next
.cache
.vscode
.idea
README.md
Lợi ích: build context nhỏ → upload nhanh, không leak secret (.env), không dirty cache (node_modules thay đổi).
6. Multi-stage Build — image gọn hơn 10×
Vấn đề: build time cần dev tool (compiler, npm devDep) — runtime không cần. Single-stage image chứa cả → lớn + nhiều attack surface.
6.1. Single-stage (BAD)
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci # cài cả devDep
RUN npm run build # cần TypeScript, webpack, ...
CMD ["npm", "start"]
# Kết quả: ~1.5 GB
6.2. Multi-stage (GOOD)
# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # cài tất cả (kể cả devDep)
COPY . .
RUN npm run build # tạo dist/
# Stage 2: production runtime
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production # chỉ prod dep
COPY --from=builder /app/dist ./dist # copy artifact từ stage 1
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
# Kết quả: ~150 MB (10× nhỏ hơn)
6.3. Build target cụ thể
docker build --target builder -t myapp:dev .
docker build --target runner -t myapp:prod .
6.4. Multi-stage cho compiled language
# Go example
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app -ldflags="-s -w" ./cmd/server
# Stage 2: distroless (no shell, no apt)
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
# Result: ~20 MB total
Distroless images (Google) chỉ chứa binary + minimal runtime, không có shell/package manager → attack surface cực thấp.
7. Image Optimization — Best Practices
7.1. Chọn base image phù hợp
| Base | Size | Use case |
|---|---|---|
| scratch | 0 MB | Static binary (Go, Rust) |
| distroless/static | 2 MB | Static binary, có CA cert |
| distroless/base | 20 MB | Dynamic linked binary |
| alpine:3.19 | 5 MB | Cần shell, apk |
| debian:slim | 28 MB | Cần glibc (Node native module) |
| ubuntu:22.04 | 78 MB | Cần apt ecosystem |
| UBI (RHEL) | 200 MB | Enterprise compliance |
7.2. Combine RUN để giảm layer
# ❌ Mỗi RUN = 1 layer
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# ✓ Combine + cleanup trong cùng layer
RUN apt-get update && \
apt-get install -y --no-install-recommends curl vim && \
rm -rf /var/lib/apt/lists/* && \
apt-get clean
Quan trọng: cleanup phải trong cùng RUN, không thì cleanup ở layer sau không xóa được data từ layer trước (immutable).
7.3. BuildKit cache mount
# syntax=docker/dockerfile:1.7
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# Mount cache cho npm — không vào image, persistent giữa builds
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline
COPY . .
CMD ["node", "server.js"]
# Enable BuildKit (default Docker 23+)
DOCKER_BUILDKIT=1 docker build -t myapp .
# Hoặc dùng buildx
docker buildx build --cache-from type=registry,ref=myapp:cache \
--cache-to type=registry,ref=myapp:cache,mode=max \
-t myapp:1.0 .
7.4. Đo và phân tích image
# Size
docker images myapp
# Layer breakdown
docker history myapp:1.0
# Detail (file system, layer wasted)
dive myapp:1.0 # https://github.com/wagoodman/dive
7.5. Reproducible build
# ❌ Không reproducible
FROM node:20 # latest 20 — thay đổi khi rebuild
# ✓ Pin version cụ thể
FROM node:20.11.1-alpine3.19
# ✓ Pin theo digest (mạnh nhất)
FROM node:20-alpine@sha256:5c7bd23a4...
8. Container Registry & Distribution
8.1. Public registries
| Registry | URL | Notes |
|---|---|---|
| Docker Hub | docker.io | Default, rate limit 100 pull/6h anonymous |
| GitHub Container Registry | ghcr.io | Free for public, integrate GHA |
| AWS ECR Public | public.ecr.aws | Free, no rate limit |
| Google Container Registry | gcr.io / pkg.dev | Migrating to Artifact Registry |
| Quay.io | quay.io | Red Hat owned, security scan |
8.2. Private registries
- AWS ECR — tích hợp IAM, lifecycle policy.
- GCP Artifact Registry — multi-format (Docker, Maven, npm, Helm).
- Azure Container Registry (ACR).
- Harbor — open source, self-host, có vulnerability scan, signing.
- JFrog Artifactory — enterprise.
8.3. Push & Pull
# Login
docker login # Docker Hub
docker login ghcr.io -u USER -p PAT # GitHub
aws ecr get-login-password | docker login --username AWS \
--password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
# Tag
docker tag myapp:1.0 ghcr.io/me/myapp:1.0
# Push
docker push ghcr.io/me/myapp:1.0
# Pull
docker pull ghcr.io/me/myapp:1.0
# Multi-arch (ARM + x86)
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/me/myapp:1.0 \
--push .
8.4. Image tagging strategy
| Tag | Use case | Pin level |
|---|---|---|
| latest | Demo, dev (avoid prod) | None — moves |
| 1, 1.5, 1.5.2 | Tag SemVer | Reasonable |
| main-abc1234 | CI build (branch + commit) | Specific commit |
| sha256:abc... | Production deploy | Immutable |
Best practice prod: pin theo SHA256 digest — không thể bị "moved" như tag.
9. Container Networking
9.1. Network drivers
- bridge (default) — virtual network, container có IP riêng, NAT ra ngoài.
- host — share network với host (no isolation).
- none — không network (chỉ loopback).
- overlay — multi-host (Swarm, K8s).
- macvlan — container có MAC address riêng, xuất hiện như device riêng trên LAN.
9.2. Bridge network mặc định
# List network
docker network ls
# bridge, host, none đã có sẵn
# Container trên bridge default — KHÔNG resolve nhau bằng tên
docker run -d --name db postgres
docker run -d --name web --link db nginx # --link deprecated
# Tốt hơn: tạo user-defined bridge
docker network create mynet
docker run -d --name db --network mynet postgres
docker run -d --name web --network mynet nginx
# Bây giờ web có thể curl http://db:5432 — DNS resolution tự động
9.3. Port mapping
# -p host_port:container_port
docker run -p 8080:80 nginx # host 8080 → container 80
docker run -p 127.0.0.1:8080:80 nginx # bind chỉ localhost
docker run -P nginx # auto map EXPOSE'd ports random
# Inspect
docker port web # xem mapping
9.4. DNS giữa container
Trong user-defined network, container resolve nhau qua tên container hoặc network alias:
docker network create app-net
docker run -d --name api --network app-net myapi
docker run -d --name web --network app-net \
-e API_URL=http://api:3000 \
myweb
# Network alias
docker run -d --network app-net --network-alias db postgres
# Container khác có thể nối "db" hoặc "<container-name>"
9.5. Inspect network
docker network inspect mynet # IP range, container, options
docker exec web ip addr # interface trong container
docker exec web cat /etc/resolv.conf # DNS server (Docker daemon)
docker exec web netstat -tlnp # ports listening
10. Volumes & Data Persistence
10.1. Storage options
| Type | Lifetime | Use case |
|---|---|---|
| Container layer (writable) | Container delete = mất | Temp data |
| Volume (named) | Persistent (docker volume) | DB data, app state |
| Bind mount | Persistent (host path) | Dev: code mount, share file với host |
| tmpfs (RAM) | Container delete = mất | Sensitive data, no disk |
10.2. Named volume
# Tạo volume
docker volume create pgdata
# Mount volume
docker run -d --name pg \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:15
# Inspect
docker volume inspect pgdata
# Mountpoint: /var/lib/docker/volumes/pgdata/_data
# List
docker volume ls
# Cleanup volume không reference
docker volume prune
10.3. Bind mount
# Bind host path → container path
docker run -d --name dev \
-v $(pwd):/app \ # mount code source vào /app
-v /app/node_modules \ # anonymous volume (shadow node_modules)
node:20 npm run dev
Use case: development hot-reload (sửa code local → container thấy ngay).
10.4. tmpfs
docker run --tmpfs /tmp:size=100M nginx
# /tmp ở RAM, max 100MB. Process write nhanh, không bao giờ ra disk.
10.5. Backup volume
# Backup volume thành tar.gz
docker run --rm \
-v pgdata:/data \
-v $(pwd):/backup \
alpine \
tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /data .
# Restore
docker run --rm \
-v pgdata:/data \
-v $(pwd):/backup \
alpine \
sh -c "cd /data && tar xzf /backup/pgdata-20240115.tar.gz"
11. Docker Compose — Multi-container
Docker Compose orchestrate nhiều container = 1 app. File compose.yaml declarative.
11.1. Compose file
# compose.yaml
services:
web:
build: . # build từ Dockerfile
image: myapp:dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://app:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- ./src:/app/src # bind mount cho hot reload
- /app/node_modules # anon volume
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- cachedata:/data
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- web
volumes:
pgdata:
cachedata:
networks:
default:
name: myapp-net
11.2. Lệnh Compose
# Start all
docker compose up # foreground (logs)
docker compose up -d # background
# Stop & remove
docker compose down # giữ volume
docker compose down -v # xóa cả volume
# Logs
docker compose logs # all services
docker compose logs -f web # follow service
# Exec
docker compose exec web sh
docker compose exec db psql -U app myapp
# Scale
docker compose up -d --scale web=3 # 3 instance web
# Build
docker compose build # build all
docker compose build web # 1 service
# Status
docker compose ps # service status
docker compose top # processes
11.3. Multi-environment với override
# compose.yaml — base
services:
web:
image: myapp:1.0
environment:
- LOG_LEVEL=info
# compose.dev.yaml — dev override
services:
web:
build: .
volumes:
- ./src:/app/src
environment:
- LOG_LEVEL=debug
- HOT_RELOAD=true
# compose.prod.yaml — prod
services:
web:
image: ghcr.io/me/myapp:1.5.0
environment:
- LOG_LEVEL=warn
deploy:
replicas: 3
# Dev
docker compose -f compose.yaml -f compose.dev.yaml up
# Prod
docker compose -f compose.yaml -f compose.prod.yaml up -d
Compose phù hợp dev environment, demo, single-host deploy. Production multi-host → Kubernetes.
12. Container Security — 10 nguyên tắc
- Run as non-root:
RUN addgroup -S app && adduser -S app -G app USER app - Read-only root filesystem:
docker run --read-only --tmpfs /tmp myapp - Drop capabilities:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp - Limit resources:
--memory=512m --cpus=0.5tránh DoS. - No privileged: tránh
--privilegedtrừ khi tuyệt đối cần. - Don't expose Docker socket:
/var/run/docker.sock= root host. - Pin base image theo digest: tag có thể bị move.
- Multi-stage build + distroless: minimize attack surface.
- Scan image: Trivy, Snyk, Grype mỗi build.
- Sign image: cosign — supply chain integrity.
12.1. Trivy scan
# Quick scan
trivy image myapp:1.0
# Severity filter, fail CI nếu HIGH
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.0
# Filesystem scan (Dockerfile)
trivy fs .
# IaC scan
trivy config . # scan terraform, k8s YAML
12.2. cosign — sign image
# Generate key pair
cosign generate-key-pair
# Sign image
cosign sign --key cosign.key ghcr.io/me/myapp:1.0
# Verify
cosign verify --key cosign.pub ghcr.io/me/myapp:1.0
Chi tiết hơn ở Chương 12 DevSecOps.
13. Bài tập
- First container: pull nginx, run với port 8080. Mount HTML file vào
/usr/share/nginx/html. Truy cập trên browser. - Build app: container hóa 1 app Node.js / Python / Go. Dockerfile multi-stage. Đo size, target < 100 MB.
- Layer caching: build app 2 lần. Verify cache hit ở COPY package.json. Sau đó sửa code, rebuild — verify chỉ rebuild từ COPY . trở đi.
- Distroless: convert Dockerfile sang multi-stage với base distroless/static. Verify image < 30 MB.
- Compose stack: viết compose.yaml cho stack 3 services: web (Node) + DB (Postgres) + cache (Redis). Health check, restart policy, named volume.
- Multi-env Compose: tạo compose.dev.yaml + compose.prod.yaml override. Dev có hot-reload, prod có replicas=3.
- Volume backup: backup Postgres data volume thành tar.gz, restore trên container mới.
- Network: tạo 2 network. Place api + db ở "backend", web + nginx ở "frontend". Web nối api qua "frontend" only — verify db không reachable từ web.
- Security harden: harden 1 image: non-root user, read-only fs, drop ALL caps, memory limit. Verify app vẫn chạy. Xác minh bằng
docker inspect. - Trivy scan: scan image của bạn. Sửa CVE HIGH bằng cách upgrade base image. Re-scan.
- Multi-arch: build image cho cả x86 và ARM (M1/M2). Push lên ghcr.io, verify cả 2 platform pull được.
- Compare: cùng app, build với (a) Ubuntu base, (b) Alpine base, (c) Distroless. So sánh size, build time, startup time, RAM.
14. Quiz
Quiz cuối Chương 5
Container KHÁC VM ở điểm cốt lõi nào?
Container isolation dựa trên Linux primitives nào?
Quy tắc cache Dockerfile để optimize build:
Multi-stage build mang lại lợi ích gì?
Pin image theo gì để reproducible build?
node:20 hoặc node:20-alpine có thể được maintainer rebuild với patch khác. Build hôm nay khác build tuần sau. Digest SHA256 là content-addressable, immutable. Trade-off: phải update digest thủ công khi muốn nâng version (Dependabot có thể tự động).Container ở user-defined bridge network có thể resolve nhau bằng:
docker network create): có DNS tự động, container resolve nhau bằng tên. Best practice: luôn tạo user-defined network thay vì dùng default.Để app trong container persist data sau khi container delete:
docker run -v pgdata:/var/lib/postgresql/data postgres. Named volume managed by Docker; bind mount = path host. Lưu ý: anonymous volume (chỉ -v /path) cũng tạo nhưng tên random, khó manage.Container security best practice:
--cap-drop=ALL --cap-add=needed, (3) --read-only --tmpfs /tmp, (4) Trivy/Snyk scan trong CI, (5) pin SHA256, (6) cosign sign. Mount Docker socket = full host root (container có thể spawn privileged container ra escape).Docker Compose phù hợp khi:
Container "scratch" (FROM scratch) phù hợp khi:
FROM scratch: image rỗng tuyệt đối, 0 byte. Chỉ chạy được static-linked binary (Go với CGO_ENABLED=0, Rust). Lợi: tiny image, không có CVE từ base. Nhược: không debug được (không shell), không tool. Khuyến cáo dùng distroless/static thay vì scratch — có CA cert, tzdata, etc., vẫn rất nhỏ.Hoàn thành Chương 5. Tiếp theo: Chương 6 — Kubernetes →