Module 14 Containers 5 labs

Docker, Container Image & Registry

Nắm vững Docker CLI từ A–Z: viết Dockerfile multi-stage chuẩn production, hiểu image layer caching, build/tag/push lên registry, scan lỗ hổng bảo mật với Trivy, tối ưu image size và orchestrate app 3-tier bằng Docker Compose.

Công cụ thực hành Docker CLI, Docker Compose, Trivy, VS Code
Nền tảng Docker Desktop (Windows/macOS) · Docker Engine (Linux) · Docker Hub · GitHub Container Registry
Thời điểm phát hành 23/05/2026
Ngày biên soạn 23/05/2026
Người biên soạn Trần Văn Hòa — Microsoft Certified Trainer (MCT)

Mục tiêu học tập

1. Lý thuyết cốt lõi

1.1. Container vs Virtual Machine

Container và VM đều cô lập workload nhưng theo cách khác nhau hoàn toàn. VM mang theo cả Guest OS (vài GB), còn container chỉ đóng gói app + dependencies, chia sẻ kernel của host — khởi động mili-giây thay vì phút, image nhỏ hơn nhiều lần.

Tiêu chíContainerVirtual Machine
StartupMili-giâyPhút
Image sizeMB (10–500 MB)GB (2–20 GB)
OS isolationChia sẻ kernel hostGuest OS riêng
DensityHàng trăm/hostHàng chục/host
PortabilityCao — chạy mọi nơi có DockerPhụ thuộc hypervisor

1.2. Kiến trúc Docker

Docker dùng kiến trúc client-server:

1.3. Image Layers & Union Filesystem

Docker dùng OverlayFS để stack các layers. Mỗi layer là delta so với layer trước, được cache và tái dùng — build lần 2 nhanh hơn nhiều nếu layers chưa thay đổi. Nguyên tắc tối ưu: đặt instruction ít thay đổi (RUN apt-get install) trước, instruction hay thay đổi (COPY . .) sau để tận dụng cache tối đa.

Quy tắc vàng Dockerfile — thứ tự instruction

  • 1. Base image  →  2. Install system deps  →  3. Copy dependency manifest (package.json / requirements.txt)  →  4. Install app deps  →  5. Copy source code  →  6. Build  →  7. CMD
  • Khi source code thay đổi, chỉ bước 5+ bị invalidate — bước 4 (npm install) vẫn được cache.

1.4. Multi-Stage Build

Multi-stage build dùng nhiều FROM trong một Dockerfile. Stage đầu (builder): cài build tools, compile. Stage cuối (runtime): chỉ copy artifact đã build — không mang theo compiler, test framework, dev dependencies. Kết quả: image nhỏ hơn 5–10x, attack surface giảm drastically.

# Pattern multi-stage chuẩn — Node.js
# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: runtime (chỉ production artifacts)
FROM node:20-alpine AS runtime
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

1.5. Image Security — Vulnerability Scanning

Trivy (Aqua Security, open-source) là công cụ scan vulnerability phổ biến nhất trong CI/CD: quét OS packages, language dependencies (npm, pip, go.mod), misconfigurations trong Dockerfile và IaC. Output liệt kê CVE với severity: CRITICAL / HIGH / MEDIUM / LOW. Tích hợp vào pipeline với --exit-code 1 để fail build tự động khi có CRITICAL CVE.

1.6. Docker Compose

Docker Compose định nghĩa và chạy multi-container app bằng một file YAML duy nhất. Compose tự tạo network riêng cho mỗi project — các service liên lạc bằng tên service (DNS tự động). Phù hợp cho dev/test local và staging; production dùng Kubernetes, nhưng Compose vẫn là tiêu chuẩn cho local development workflow.

2. Thực hành (Labs)

LAB-066

Viết Dockerfile production (multi-stage, non-root user)

Docker CLI · VS Code · Git

🎯 Mục tiêu: Viết Dockerfile multi-stage cho Node.js Express API, so sánh image size giữa single-stage và multi-stage, đảm bảo container chạy với non-root user.

🧰 Công cụ / nền tảng: Docker Desktop (Windows/macOS) hoặc Docker Engine (Linux), VS Code, Git.

📦 Chuẩn bị: Chạy docker version xác nhận Docker đang hoạt động; docker info | grep "Server Version" kiểm tra version.

▶️ Các bước:

# 1. Tạo project Node.js API
mkdir docker-lab066 && cd docker-lab066
git init

cat > package.json << 'EOF'
{
  "name": "demo-api",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/index.js",
    "build": "mkdir -p dist && cp src/index.js dist/"
  },
  "dependencies": { "express": "^4.18.2" },
  "devDependencies": { "nodemon": "^3.0.0" }
}
EOF

mkdir src
cat > src/index.js << 'EOF'
const express = require('express');
const app = express();
app.get('/healthz', (_, res) => res.json({ status: 'ok', pid: process.pid }));
app.listen(3000, () => console.log('Server running on port 3000'));
EOF
# 2. Dockerfile single-stage (để so sánh size)
cat > Dockerfile.single << 'EOF'
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
EOF

# 3. Dockerfile multi-stage production
cat > Dockerfile << 'EOF'
# ---- Stage 1: builder ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Stage 2: runtime ----
FROM node:20-alpine AS runtime
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
EOF

# 4. .dockerignore — quan trọng!
cat > .dockerignore << 'EOF'
node_modules
npm-debug.log
.git
.gitignore
*.md
.env
EOF
# 5. Build cả hai để so sánh
docker build -f Dockerfile.single -t demo-api:single .
docker build -f Dockerfile        -t demo-api:multi  .

# 6. So sánh size
docker images demo-api --format "table {{.Tag}}\t{{.Size}}"
# TAG      SIZE
# multi    ~120 MB   <-- nhỏ hơn nhiều
# single   ~1.1 GB

# 7. Chạy và kiểm tra
docker run -d --name api-test -p 3000:3000 demo-api:multi
sleep 2
curl http://localhost:3000/healthz
# {"status":"ok","pid":1}

# 8. Xác nhận KHÔNG chạy root
docker exec api-test whoami
# appuser

# 9. Xem layer history
docker history demo-api:multi

🖥️ Đối chiếu GUI: Docker Desktop → Images → so sánh cột Size. Click vào image để xem layers.

✅ Kết quả mong đợi: demo-api:multi nhỏ hơn demo-api:single ít nhất 5x; curl /healthz trả về JSON; whoami in ra appuser.

🧹 Cleanup:

docker stop api-test && docker rm api-test
docker rmi demo-api:multi demo-api:single
cd .. && rm -rf docker-lab066
LAB-067

Build, tag, push và pull image lên Docker Hub & GHCR

Docker CLI · VS Code · Git

🎯 Mục tiêu: Build image với semantic version tag, push lên Docker Hub và GitHub Container Registry (GHCR), pull về từ registry, hiểu image naming convention.

🧰 Công cụ / nền tảng: Docker CLI, tài khoản Docker Hub, tài khoản GitHub + Personal Access Token (PAT) với quyền write:packages.

📦 Chuẩn bị: Tạo tài khoản Docker Hub tại hub.docker.com; tạo GitHub PAT tại GitHub → Settings → Developer settings → Personal access tokens.

▶️ Các bước:

# Thiết lập biến — thay bằng username thực của bạn
DOCKER_USER="YOUR_DOCKERHUB_USERNAME"
GITHUB_USER="YOUR_GITHUB_USERNAME"
IMAGE_NAME="demo-api"
VERSION="1.0.0"

# 1. Build với multiple tags
docker build \
  -t ${DOCKER_USER}/${IMAGE_NAME}:${VERSION} \
  -t ${DOCKER_USER}/${IMAGE_NAME}:latest \
  -f Dockerfile .

docker images ${DOCKER_USER}/${IMAGE_NAME}

# 2. Login Docker Hub
docker login
# Nhập Docker Hub username và password/access token

# 3. Push lên Docker Hub
docker push ${DOCKER_USER}/${IMAGE_NAME}:${VERSION}
docker push ${DOCKER_USER}/${IMAGE_NAME}:latest
echo "Xem tại: https://hub.docker.com/r/${DOCKER_USER}/${IMAGE_NAME}"
# 4. Tag cho GHCR — format: ghcr.io/GITHUB_USER/IMAGE:TAG
docker tag ${DOCKER_USER}/${IMAGE_NAME}:${VERSION} \
  ghcr.io/${GITHUB_USER}/${IMAGE_NAME}:${VERSION}

docker tag ${DOCKER_USER}/${IMAGE_NAME}:latest \
  ghcr.io/${GITHUB_USER}/${IMAGE_NAME}:latest

# 5. Login GHCR bằng PAT
export GITHUB_PAT="ghp_xxxxxxxxxxxxxxxxxxxx"   # thay bằng PAT thực
echo $GITHUB_PAT | docker login ghcr.io \
  --username ${GITHUB_USER} --password-stdin

# 6. Push lên GHCR
docker push ghcr.io/${GITHUB_USER}/${IMAGE_NAME}:${VERSION}
docker push ghcr.io/${GITHUB_USER}/${IMAGE_NAME}:latest
echo "Xem tại: https://github.com/${GITHUB_USER}?tab=packages"
# 7. Test pull từ Docker Hub
docker rmi ${DOCKER_USER}/${IMAGE_NAME}:${VERSION}
docker pull ${DOCKER_USER}/${IMAGE_NAME}:${VERSION}

# 8. Kiểm tra metadata
docker inspect ${DOCKER_USER}/${IMAGE_NAME}:${VERSION} \
  --format 'ExposedPorts: {{json .Config.ExposedPorts}}'
# ExposedPorts: {"3000/tcp":{}}

docker inspect ${DOCKER_USER}/${IMAGE_NAME}:${VERSION} \
  --format 'User: {{.Config.User}}'
# User: appuser

🖥️ Đối chiếu GUI: Docker Hub: đăng nhập hub.docker.com → Repositories. GHCR: GitHub profile → Packages.

✅ Kết quả mong đợi: Image xuất hiện trên Docker Hub với tag 1.0.0latest; GHCR hiển thị package; pull thành công; inspect trả về metadata đúng.

🧹 Cleanup:

docker rmi ${DOCKER_USER}/${IMAGE_NAME}:${VERSION} ${DOCKER_USER}/${IMAGE_NAME}:latest
docker rmi ghcr.io/${GITHUB_USER}/${IMAGE_NAME}:${VERSION} ghcr.io/${GITHUB_USER}/${IMAGE_NAME}:latest
docker logout
docker logout ghcr.io
LAB-068

Docker Compose app 3-tier (Nginx + Node.js API + PostgreSQL)

Docker Compose · VS Code · Git

🎯 Mục tiêu: Viết docker-compose.yml cho app 3-tier: Nginx (reverse proxy/frontend) + Node.js API (backend) + PostgreSQL (database); cấu hình network isolation (frontend không talk trực tiếp DB), named volumes để persist data.

🧰 Công cụ / nền tảng: Docker Compose v2 (docker compose), Docker Desktop.

📦 Chuẩn bị: docker compose version xác nhận Compose v2; tạo thư mục docker-lab068/.

▶️ Các bước — tạo cấu trúc project:

mkdir docker-lab068 && cd docker-lab068
mkdir -p frontend backend db

# Backend: Express API + PostgreSQL
cat > backend/package.json << 'EOF'
{
  "name": "backend-api", "version": "1.0.0",
  "dependencies": { "express": "^4.18.2", "pg": "^8.11.0" }
}
EOF

cat > backend/index.js << 'EOF'
const express = require('express');
const { Pool } = require('pg');
const pool = new Pool({
  host: process.env.DB_HOST || 'db',
  database: process.env.DB_NAME || 'appdb',
  user: process.env.DB_USER || 'appuser',
  password: process.env.DB_PASSWORD || 'apppassword'
});
const app = express();
app.use(express.json());
app.get('/healthz', (_, res) => res.json({ status: 'ok' }));
app.get('/api/users', async (_, res) => {
  const r = await pool.query('SELECT id, name FROM users ORDER BY id');
  res.json(r.rows);
});
app.post('/api/users', async (req, res) => {
  const { name } = req.body;
  const r = await pool.query(
    'INSERT INTO users(name) VALUES($1) RETURNING *', [name]);
  res.status(201).json(r.rows[0]);
});
app.listen(3000, () => console.log('Backend running on :3000'));
EOF

cat > backend/Dockerfile << 'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install && npm cache clean --force
COPY . .
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
CMD ["node", "index.js"]
EOF
# Frontend: HTML static + Nginx config
cat > frontend/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>3-Tier Demo</title></head>
<body>
  <h1>User List</h1>
  <ul id="list"></ul>
  <input id="nameInput" placeholder="Enter name">
  <button id="addBtn">Add User</button>
  <script>
    async function loadUsers() {
      const res = await fetch('/api/users');
      const users = await res.json();
      const ul = document.getElementById('list');
      ul.textContent = '';
      users.forEach(u => {
        const li = document.createElement('li');
        li.textContent = u.id + ': ' + u.name;
        ul.appendChild(li);
      });
    }
    document.getElementById('addBtn').addEventListener('click', async () => {
      const name = document.getElementById('nameInput').value.trim();
      if (!name) return;
      await fetch('/api/users', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({ name })
      });
      loadUsers();
    });
    loadUsers();
  </script>
</body>
</html>
EOF

cat > frontend/nginx.conf << 'EOF'
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;
    location / { try_files $uri /index.html; }
    location /api/ { proxy_pass http://backend:3000; }
    location /healthz { proxy_pass http://backend:3000/healthz; }
}
EOF

# DB seed script
cat > db/init.sql << 'EOF'
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);
INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');
EOF
# docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: "3.9"
services:

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppassword
    volumes:
      - db_data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks: [backend_net]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  backend:
    build: ./backend
    environment:
      DB_HOST: db
      DB_NAME: appdb
      DB_USER: appuser
      DB_PASSWORD: apppassword
    networks: [backend_net, frontend_net]
    depends_on:
      db: { condition: service_healthy }
    restart: unless-stopped

  frontend:
    image: nginx:1.25-alpine
    ports: ["8080:80"]
    volumes:
      - ./frontend/index.html:/usr/share/nginx/html/index.html:ro
      - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks: [frontend_net]   # KHÔNG có backend_net
    depends_on: [backend]
    restart: unless-stopped

networks:
  frontend_net:
  backend_net:

volumes:
  db_data:
EOF
# Khởi động stack
docker compose up -d --build

# Xem trạng thái
docker compose ps
# NAME       IMAGE              STATUS
# frontend   nginx:1.25-alpine  Up    0.0.0.0:8080->80/tcp
# backend    backend-api        Up    3000/tcp
# db         postgres:16-alpine Up (healthy)

# Kiểm tra API
curl http://localhost:8080/healthz
# {"status":"ok"}

curl http://localhost:8080/api/users
# [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"}]

# Thêm user mới
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"DevOps Engineer"}'
# {"id":4,"name":"DevOps Engineer"}

# Kiểm tra network isolation
docker compose exec frontend ping db 2>&1 | head -2
# ping: bad address 'db'   <-- đúng, frontend không trong backend_net

# Logs
docker compose logs --tail=20 backend

🖥️ Đối chiếu GUI: Docker Desktop → Containers → xem 3 container. Mở http://localhost:8080 trong browser để xem UI và test thêm user.

✅ Kết quả mong đợi: 3 service Up; /api/users trả về 3 users từ DB; POST thành công; frontend không ping được DB; data persist sau docker compose restart.

🧹 Cleanup:

docker compose down -v          # xóa containers + volumes
docker compose down --rmi local  # xóa thêm images đã build
cd .. && rm -rf docker-lab068
LAB-069

Scan image vulnerability bằng Trivy

Trivy · Docker CLI · VS Code

🎯 Mục tiêu: Cài Trivy, scan image cũ nhiều CVE và image alpine ít CVE, đọc report, tích hợp Trivy vào build workflow để fail tự động khi có CRITICAL vulnerability.

🧰 Công cụ / nền tảng: Trivy CLI, Docker. Hỗ trợ Windows/macOS/Linux.

📦 Chuẩn bị: Cài Trivy:

# Linux / WSL2
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
  | sh -s -- -b /usr/local/bin

# macOS (Homebrew)
brew install trivy

# Windows (Scoop)
scoop bucket add extras && scoop install trivy

# Kiểm tra
trivy --version
# trivy version 0.51.x

▶️ Các bước:

# 1. Scan image Debian cũ — nhiều CVE (lần đầu download ~200MB vuln DB)
trivy image --severity HIGH,CRITICAL python:3.9-slim-buster
# Output: bảng CVE với Library | Vulnerability | Severity | Installed | Fixed

# 2. Scan image alpine mới — ít CVE hơn nhiều
trivy image --severity HIGH,CRITICAL python:3.12-alpine
# Output: số lượng CVE giảm đáng kể

# 3. Scan image tự build từ LAB-066 (dùng lại Dockerfile)
mkdir -p /tmp/trivy-test && cd /tmp/trivy-test
cat > Dockerfile << 'EOF'
FROM node:20-alpine AS builder
WORKDIR /app
RUN echo '{"name":"demo","version":"1.0.0","dependencies":{"express":"^4.18.2"}}' > package.json
RUN npm install
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
CMD ["node", "-e", "require('express')()"]
EOF

docker build -t demo-scan:test .
trivy image --severity HIGH,CRITICAL demo-scan:test
# 4. Export JSON report
trivy image --format json --output trivy-report.json demo-scan:test

# Đọc tóm tắt từ JSON
python3 -c "
import json
data = json.load(open('trivy-report.json'))
results = data.get('Results', [])
total = sum(len(r.get('Vulnerabilities') or []) for r in results)
high = sum(1 for r in results for v in (r.get('Vulnerabilities') or []) if v['Severity']=='HIGH')
crit = sum(1 for r in results for v in (r.get('Vulnerabilities') or []) if v['Severity']=='CRITICAL')
print(f'Total={total}  HIGH={high}  CRITICAL={crit}')
"
# 5. Scan Dockerfile misconfiguration
trivy config --severity HIGH,CRITICAL /tmp/trivy-test/Dockerfile
# Warning: "Image tag should use specific digest" hoặc issues nếu có

# 6. Tích hợp CI: fail build nếu có CRITICAL CVE
trivy image \
  --exit-code 1 \
  --severity CRITICAL \
  --no-progress \
  demo-scan:test
echo "Exit code: $?   (0=no CRITICAL, 1=CRITICAL found — pipeline sẽ fail)"

# 7. So sánh nhanh hai base image
echo "=== python:3.9-slim-buster ==="
trivy image --severity CRITICAL --no-progress python:3.9-slim-buster 2>&1 | grep "Total:"

echo "=== python:3.12-alpine ==="
trivy image --severity CRITICAL --no-progress python:3.12-alpine 2>&1 | grep "Total:"

🖥️ Đối chiếu GUI: Tạo HTML report: trivy image --format template --template "@contrib/html.tpl" -o report.html IMAGE_NAME rồi mở file trong browser.

✅ Kết quả mong đợi: python:3.9-slim-buster có nhiều HIGH/CRITICAL; python:3.12-alpine ít hơn rõ rệt; demo-scan:test có 0 CRITICAL (exit code 0); JSON report tạo đúng; trivy config phát hiện ít nhất 1 misconfiguration.

🧹 Cleanup:

docker rmi demo-scan:test
rm -rf /tmp/trivy-test trivy-report.json report.html
LAB-070

Tối ưu image size — từ 1 GB xuống dưới 100 MB

Docker CLI · VS Code · Git

🎯 Mục tiêu: Áp dụng 4 kỹ thuật tối ưu image size theo thứ tự tăng dần: full base → slim base → alpine base → multi-stage alpine; đo size sau từng bước, xác nhận app vẫn hoạt động đúng.

🧰 Công cụ / nền tảng: Docker CLI, docker history.

📦 Chuẩn bị: Tạo thư mục docker-lab070/; kết nối Internet để pull base images.

▶️ Các bước — xây dựng 4 version và đo size:

mkdir docker-lab070 && cd docker-lab070

# App Python Flask đơn giản
cat > app.py << 'EOF'
from flask import Flask, jsonify
app = Flask(__name__)

@app.route('/healthz')
def health():
    return jsonify(status='ok')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
EOF

cat > requirements.txt << 'EOF'
flask==3.0.0
gunicorn==21.2.0
EOF

# .dockerignore
cat > .dockerignore << 'EOF'
__pycache__
*.pyc
.git
*.md
Dockerfile*
EOF
# ---- V1: python:3.12 full (worst case) ----
cat > Dockerfile.v1 << 'EOF'
FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
EOF
docker build -f Dockerfile.v1 -t demo-size:v1 .

# ---- V2: python:3.12-slim ----
cat > Dockerfile.v2 << 'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
EOF
docker build -f Dockerfile.v2 -t demo-size:v2 .

# ---- V3: python:3.12-alpine ----
cat > Dockerfile.v3 << 'EOF'
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
EOF
docker build -f Dockerfile.v3 -t demo-size:v3 .

# ---- V4: multi-stage alpine (best) ----
cat > Dockerfile.v4 << 'EOF'
FROM python:3.12-alpine AS builder
WORKDIR /app
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

FROM python:3.12-alpine AS runtime
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
RUN adduser -D appuser && chown -R appuser /app
USER appuser
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
EOF
docker build -f Dockerfile.v4 -t demo-size:v4 .
# So sánh tất cả versions
echo "===== Image Size Comparison ====="
docker images demo-size --format "table {{.Tag}}\t{{.Size}}" | sort
# TAG   SIZE
# v1    ~1.05 GB
# v2    ~200 MB
# v3    ~80 MB
# v4    ~75 MB

# Xem layer breakdown của V4
docker history demo-size:v4 \
  --format "table {{.Size}}\t{{.CreatedBy}}" | head -10

# Test V4 hoạt động đúng
docker run -d --name size-test -p 5000:5000 demo-size:v4
sleep 3
curl http://localhost:5000/healthz
# {"status":"ok"}

# Kiểm tra non-root
docker exec size-test whoami
# appuser

🖥️ Đối chiếu GUI: Docker Desktop → Images → so sánh cột Size. Click từng image → xem Layers tab.

✅ Kết quả mong đợi: V1 ~1 GB; V4 ~75 MB (giảm trên 90%); curl /healthz trả về JSON đúng; whoami = appuser; bảng so sánh 4 version rõ ràng.

🧹 Cleanup:

docker stop size-test && docker rm size-test
docker rmi demo-size:v1 demo-size:v2 demo-size:v3 demo-size:v4
cd .. && rm -rf docker-lab070

3. Tình huống doanh nghiệp thực tế

Bối cảnh

Một team fintech triển khai microservices trên Kubernetes. Sau security audit phát hiện: (1) base image node:18 full — 1.1 GB mỗi service, CI pipeline mất 8 phút; (2) container chạy root; (3) 12 CVE CRITICAL trong image cũ không ai biết; (4) tất cả image đều tag latest — không ai biết production đang chạy version nào.

Cách xử lý

  • Multi-stage Dockerfile: build stage dùng node:20, runtime stage dùng node:20-alpine + non-root user → image từ 1.1 GB xuống 120 MB; CI pipeline từ 8 phút xuống 3 phút.
  • Semantic versioning tag: đổi sang v1.2.3-abc1234 (version + git short SHA) → biết chính xác code version nào đang chạy, rollback dễ dàng.
  • Trivy trong CI: thêm step trivy image --exit-code 1 --severity CRITICAL sau mỗi build → pipeline fail tự động nếu có CVE CRITICAL; fix 12 CVE bằng cách update base image và dependencies.
  • Private registry (GHCR/ECR): chuyển từ Docker Hub public sang GHCR với image scan tích hợp; policy: chỉ pull image từ registry nội bộ (OPA/Kyverno enforce).
  • Kết quả: CI pipeline nhanh hơn 60%; 0 CRITICAL CVE; audit trail đầy đủ qua immutable image tags; team deploy tự tin và nhanh hơn.

📚 Nguồn tham khảo

Module 13: Multi-cloud Governance Module 15: Kubernetes Core
Zalo