·

Quy trình Thực tế: Debug Container & Tối ưu Image

Đi qua một quy trình thực tế dùng Docker MCP để AI agent quản lý container, image và log từ đầu đến cuối.

Bốn bài trước trong module này đã trang bị từng mảnh ghép riêng lẻ: Docker MCP là gì, cách cài với Claude Code, OpenCode, Gemini CLI, và Cursor. Bài này là bài capstone (bài tổng kết) — ghép toàn bộ lại thành một case study đầu-cuối, mô phỏng đúng những gì một backend/platform engineer sẽ làm khi nhận báo cáo "service chạy chậm bất thường và image quá nặng" trong tuần trước khi release.

Chúng ta sẽ đi theo một tình huống cụ thể xuyên suốt cả ba bước: từ container đang lỗi, qua chẩn đoán root cause trên cả Dockerfile, compose, và runtime config, đến rebuild và verify image production đã được tối ưu. Đọc xong bài này, bạn nên áp dụng được ngay quy trình tương tự cho service thật của team mình.

Tổng Quan Workflow: Từ Container Lỗi Đến Image Production Được Tối Ưu

Trước khi vào chi tiết, hãy hình dung toàn cảnh workflow:

[Container báo lỗi / chạy chậm] --> [Agent thu thập log, exit code, inspect]
                                                    |
                                                    v
                          [Agent phân tích root cause: Dockerfile / Compose / Runtime]
                                                    |
                                                    v
                                  [Dev áp dụng fix + rebuild image]
                                                    |
                                                    v
                            [Agent verify + so sánh size/performance trước-sau]
                                                    |
                                                    v
                                    [Image mới được duyệt để deploy]

Tình huống xuyên suốt bài viết

Để cụ thể hoá, dùng một ticket thật giống như bạn sẽ thấy trên Jira:

Ticket #7302 — Service "image-processor" khởi động chậm (>90s) và image nặng 2.4GB
Môi trường: staging, build từ CI pipeline mới nhất.
Quan sát: container mất hơn 90 giây để chuyển sang trạng thái "healthy" (trước đây chỉ mất ~15 giây). Image size tăng từ 800MB lên 2.4GB sau vài lần thêm dependency mới trong 2 tháng gần đây. CI build time cũng tăng gấp 3 lần.
Mục tiêu: tìm nguyên nhân startup chậm, và tối ưu lại image để giảm size + build time, không đổi behavior của service.

Đây là loại ticket rất thực tế: không phải một bug rõ ràng có exception, mà là một dạng "technical debt tích tụ dần" — thứ dễ bị trì hoãn xử lý vì không gây outage ngay, nhưng ảnh hưởng tích lũy tới tốc độ deploy và chi phí hạ tầng của cả team.

Phân vai: ai làm gì trong workflow này

  • Engineer (con người) đưa ra mục tiêu và ràng buộc: mô tả triệu chứng, ràng buộc không được đổi behavior, và quyết định cuối cùng có nên rebuild base image hay không.
  • Agent (qua Docker MCP) thực hiện: thu thập log/exit code/inspect, đọc Dockerfile và compose, chạy build thử, so sánh size/thời gian trước-sau — toàn bộ phần thao tác lặp lại và tốn thời gian.
  • Con người vẫn quyết định: agent có thể đề xuất "đổi base image sang alpine", nhưng quyết định liệu thay đổi này có ảnh hưởng tới dependency native (ví dụ package Python cần biên dịch lại với musl libc) là trách nhiệm của engineer, không giao hẳn cho agent.

Mẹo: Trước khi bắt đầu, viết rõ ràng buộc "không đổi behavior" ngay trong prompt đầu tiên gửi agent — nếu không, agent tối ưu size có thể vô tình đổi cả version dependency hoặc bỏ một bước cài đặt tưởng chừng không cần thiết nhưng thực ra ảnh hưởng tới hành vi runtime.

Bước 1: Tái Hiện Lỗi Và Thu Thập Log, Exit Code, Inspect Output

Mục tiêu bước này: có bằng chứng cụ thể về thời gian startup thực tế, không dựa vào cảm giác "chậm hơn trước" mà cần số liệu chính xác để so sánh trước/sau khi fix.

Các hành động agent cần thực hiện

  1. list_containers để xác nhận container image-processor hiện tại và trạng thái.
  2. inspect_container để lấy State.StartedAt và log health check (State.Health.Log) — từ đó tính chính xác thời gian tới khi container chuyển "healthy".
  3. get_logs với tail hợp lý để đọc log giai đoạn startup, tìm bước nào chiếm nhiều thời gian nhất (load model, kết nối dependency, warm-up cache...).
  4. get_stats để kiểm tra CPU/memory usage trong lúc startup — startup chậm do CPU-bound (biên dịch/parse gì đó lúc chạy) khác hẳn nguyên nhân do I/O chờ network.

Prompt mẫu

Container "image-processor" đang mất hơn 90 giây để chuyển sang
trạng thái healthy, trước đây chỉ mất khoảng 15 giây. Hãy:
1. Restart container này và đo chính xác thời gian từ lúc start tới
   lúc health check pass lần đầu tiên.
2. Trong lúc container đang startup, lấy log theo từng giai đoạn (mỗi
   5 giây một lần trong 90 giây đầu) để xác định giai đoạn nào chiếm
   nhiều thời gian nhất.
3. Lấy CPU và memory usage trong suốt quá trình startup.
4. Inspect container để lấy cấu hình health check hiện tại (interval,
   timeout, start_period) — startup chậm có thể chỉ là vấn đề đo đạc
   nếu start_period đang set quá ngắn khiến health check fail nhiều
   lần trước khi thực sự pass.
5. Tổng hợp timeline: giai đoạn nào (theo thời gian) tương ứng với
   log gì, và giai đoạn nào chậm bất thường.

Ví dụ kết quả agent tổng hợp

TIMELINE STARTUP - container "image-processor"

0s     - Container started
0-3s   - Log: "Loading base config..."
3-8s   - Log: "Connecting to Redis..." -> connected ở giây thứ 8
8-85s  - Log: "Loading ML model from /app/models/detector.onnx..."
         (không có log nào khác trong suốt 77 giây này)
85-90s - Log: "Model loaded. Server listening on :8080"
90s    - Health check pass

CPU usage: gần 100% trong khoảng 8s-85s (giai đoạn load model)
Memory: tăng dần từ 200Mi lên 1.8Gi trong cùng khoảng thời gian

=> Giai đoạn "Loading ML model" chiếm 77/90 giây (86% tổng thời
gian startup), và tương ứng với CPU gần bão hoà + memory tăng mạnh —
đây là nghi phạm chính, không phải vấn đề network hay dependency.

Đây là bằng chứng cụ thể (không phải suy đoán) để chuyển sang bước phân tích root cause.

Mẹo: Luôn yêu cầu agent đo bằng số liệu thật (timestamp, CPU%, memory) thay vì chỉ đọc log một lần rồi kết luận — với bug về performance/startup time, một lần đo duy nhất dễ bị nhiễu bởi cache lạnh hay tải máy lúc đó, nên nếu có thể, yêu cầu chạy lại 2-3 lần để xác nhận số liệu nhất quán.

Bước 2: AI Phân Tích Root Cause Trên Dockerfile, Compose Và Runtime Config

Sau khi biết chính xác giai đoạn "Loading ML model" là nguyên nhân chậm, bước tiếp theo là tìm hiểu vì sao — có thể nằm ở cách Dockerfile đóng gói model, cấu hình runtime, hoặc cách compose khởi động dependency theo thứ tự sai.

Đọc Dockerfile để tìm nguyên nhân đóng gói

Đọc Dockerfile của service "image-processor" (tôi paste nội dung).
Model file "/app/models/detector.onnx" có được copy vào image ở
build stage nào? Model có bị decompress/convert format ngay trong
CMD/ENTRYPOINT lúc container start (thay vì làm sẵn lúc build) không?
Đây có thể là nguyên nhân model "load" chậm mỗi lần container start.

Giả sử agent phát hiện Dockerfile có đoạn:

COPY models/detector.onnx.gz /app/models/detector.onnx.gz
ENTRYPOINT ["sh", "-c", "gunzip /app/models/detector.onnx.gz && exec node server.js"]

Đây chính là root cause: model được lưu ở dạng compressed (.gz) trong image, và bị decompress mỗi lần container start — tốn CPU và thời gian mỗi lần restart/scale, thay vì chỉ tốn một lần lúc build image.

Kiểm tra runtime config và thứ tự khởi động trong compose

Đọc compose.yaml của service "image-processor". Service này có
depends_on Redis không, và có dùng "condition: service_healthy" hay
chỉ "depends_on" đơn giản (chỉ đảm bảo thứ tự start container, không
đảm bảo Redis đã sẵn sàng nhận connection)? Đối chiếu với log ở bước
trước, giai đoạn "Connecting to Redis" mất 5 giây (giây 3-8) — kiểm
tra xem có phải do retry logic chờ Redis sẵn sàng không.

Trường hợp này 5 giây kết nối Redis không phải vấn đề chính (so với 77 giây load model), nhưng vẫn đáng ghi nhận để tối ưu thêm nếu cần — ví dụ thêm condition: service_healthy vào depends_on để tránh retry loop không cần thiết.

Tổng hợp root cause đầy đủ

ROOT CAUSE - Ticket #7302

Nguyên nhân chính (86% thời gian startup): model ONNX được lưu dạng
compressed (.gz) trong image, và bị gunzip mỗi lần container start
trong ENTRYPOINT, thay vì được decompress sẵn lúc build image.

Nguyên nhân phụ (nhỏ, 5s): compose dùng "depends_on: redis" không
kèm "condition: service_healthy", dẫn tới vài lần retry connect
trước khi Redis thực sự sẵn sàng.

Về image size (2.4GB): ngoài model .gz (180MB), phần lớn size còn
lại (~1.5GB) đến từ base image "python:3.11" (bản full, không phải
slim) và một số build dependency (gcc, build-essential) không được
xoá sau khi build các package cần biên dịch native.

Báo cáo dạng này — tách rõ nguyên nhân chính/phụ, kèm số liệu — là chuẩn bạn nên yêu cầu agent tuân theo mọi lúc, để tránh tình trạng "sửa nhầm chỗ nhỏ, bỏ qua chỗ ảnh hưởng lớn nhất".

Mẹo: Khi root cause nằm ở nhiều lớp (Dockerfile, compose, runtime), luôn yêu cầu agent xếp hạng theo mức độ ảnh hưởng (% thời gian, % size) trước khi đề xuất fix — tránh trường hợp team dồn effort sửa một nguyên nhân phụ chỉ vì nó dễ sửa hơn, bỏ qua nguyên nhân chính gây ảnh hưởng lớn nhất.

Bước 3: Rebuild, Verify Và Thu Nhỏ Image Cuối Cùng

Sau khi đã có root cause rõ ràng, bước cuối là áp dụng fix, rebuild, và verify bằng số liệu thật rằng cả performance và size đã cải thiện đúng như kỳ vọng — không chỉ tin vào lý thuyết.

Áp dụng fix cho từng nguyên nhân

Prompt yêu cầu agent đề xuất Dockerfile đã sửa:

Dựa trên root cause đã xác định, hãy đề xuất Dockerfile mới cho
service "image-processor" với các thay đổi:
1. Decompress model .onnx ngay lúc build (RUN gunzip), không để
   trong ENTRYPOINT lúc runtime.
2. Đổi base image từ "python:3.11" sang "python:3.11-slim", giữ
   nguyên các package Python hiện có trong requirements.txt.
3. Dùng multi-stage build: stage builder cài build-essential/gcc để
   compile các package native, stage runtime chỉ copy virtual env đã
   build xong, không mang theo build tool.
4. Cập nhật compose.yaml: thêm "condition: service_healthy" vào
   depends_on cho Redis.
Xuất ra Dockerfile và đoạn compose.yaml đã sửa, kèm giải thích mỗi
thay đổi.

Ví dụ Dockerfile sau khi áp dụng đề xuất:

FROM python:3.11-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential gcc \
    && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN python -m venv /opt/venv \
    && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

FROM python:3.11-slim AS runtime
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY models/detector.onnx.gz /tmp/detector.onnx.gz
RUN gunzip /tmp/detector.onnx.gz -c > /app/models/detector.onnx && rm /tmp/detector.onnx.gz
COPY . .
CMD ["python", "server.py"]

Verify bằng số liệu thật, so sánh trước-sau

Build image mới với Dockerfile đã sửa, tag "image-processor:optimized".
So sánh với image cũ "image-processor:latest" (bản 2.4GB) theo các
tiêu chí:
1. Image size (docker images).
2. Số layer và size từng layer (docker history), chỉ ra layer nào
   giảm nhiều nhất.
3. Thời gian build image từ đầu (build time, no cache).
4. Thời gian container chuyển sang healthy sau khi start (lặp lại
   đúng cách đo ở Bước 1, chạy 3 lần lấy trung bình).
Xuất kết quả dạng bảng so sánh trước/sau cho cả 4 tiêu chí.

Ví dụ bảng kết quả agent trả về:

Tiêu chí Trước (latest) Sau (optimized) Cải thiện
Image size 2.4GB 780MB Giảm 67%
Build time (no cache) 4m 20s 2m 05s Giảm 52%
Thời gian tới healthy (trung bình 3 lần) 91s 9s Giảm 90%
Layer lớn nhất python:3.11 full (920MB) python:3.11-slim + venv (410MB)

Đảm bảo không đổi behavior trước khi duyệt

Trước khi coi là hoàn thành, luôn có một bước kiểm tra behavior không đổi — đây là bước con người nên trực tiếp xác nhận thêm, không chỉ dựa vào agent:

Chạy container "image-processor:optimized" và gửi 5 request test
(tôi cung cấp payload mẫu) tới endpoint xử lý ảnh chính. So sánh
response (kết quả detection, thời gian xử lý mỗi request) với kết
quả từ image cũ "image-processor:latest" chạy song song trên port
khác. Xác nhận kết quả detection giống nhau (cùng bounding box, cùng
confidence score trong sai số cho phép).

Nếu kết quả detection khớp và chỉ khác biệt ở size/tốc độ startup, image mới đủ điều kiện để đưa qua CI/CD pipeline chính thức và review trước khi merge.

Mẹo: Luôn giữ lại image cũ (tag riêng, chưa xoá) trong ít nhất một chu kỳ release sau khi deploy bản tối ưu — nếu phát hiện vấn đề behavior không lường trước ở production, bạn có thể rollback ngay bằng cách đổi lại tag, không cần build lại từ đầu trong tình huống gấp.

Tips

Workflow ba bước — thu thập bằng chứng, phân tích root cause đa lớp, rebuild và verify bằng số liệu — không phải quy trình mới, đó chính là cách một senior engineer debug performance/size issue đã làm từ trước khi có AI. Điều Docker MCP thay đổi là tốc độ thu thập bằng chứng và khả năng đối chiếu nhiều nguồn dữ liệu (log, inspect, stats, Dockerfile, compose) trong một lượt, để con người tập trung vào phần quan trọng nhất: quyết định thay đổi nào an toàn để đưa vào production.

  • Luôn đo bằng số liệu thật (thời gian, size, CPU/memory) trước và sau mỗi thay đổi, không chấp nhận kết luận định tính.
  • Khi root cause có nhiều lớp, yêu cầu agent xếp hạng theo mức độ ảnh hưởng trước khi đề xuất fix.
  • Với mọi thay đổi tối ưu Dockerfile, luôn có bước xác nhận behavior không đổi bằng test thật, không chỉ tin vào build pass.
  • Giữ lại image cũ ít nhất một chu kỳ release để có đường rollback nhanh.
  • Đóng gói lại toàn bộ workflow này (các prompt đã dùng) thành tài liệu chuẩn của team, để áp dụng lại cho service khác có vấn đề tương tự.

Mẹo: Sau mỗi lần tối ưu image thành công, lưu lại bảng so sánh trước-sau và Dockerfile cũ/mới vào một file docs/image-optimization-log.md trong repo — đây vừa là tài liệu học tập cho team, vừa là bằng chứng cụ thể để thuyết phục stakeholder khi cần xin thời gian làm technical debt tương tự cho các service khác.