jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Docker for Developers · Part 7 — Optimizing & Securing Images

Tối ưu và harden image với base nhỏ, cache, multi-stage, non-root, secret mount, .dockerignore và quét lỗ hổng.

8 MIN READ Updated JUL 11, 2026

Đây là Phần 7 của chặng nền tảng về Docker → Compose → Kubernetes. Ở Phần 2 bạn đã học build image — layer, cache, multi-stage, .dockerignore. Ở Phần 6 bạn điều phối stack nhiều service với profile và health check.

Image chạy được chưa phải image tốt. Layer phình làm mọi pull và deploy chậm; mặc định root mở rộng vùng ảnh hưởng; secret nhúng vào layer sống mãi trong docker history; base image không ghim khiến build hôm nay khác build tuần sau. Phần này giúp image nhỏ, nhanh, tái lập và an toàn.

Mỗi phần kết thúc bằng bài tập; chạy lệnh trên máy thật.

Checklist review image kiểu senior:

  • Reproducible — base/tag rõ, lockfile được copy đúng, build không phụ thuộc file rác ngoài context.
  • Fast — dependency layer cache tốt; source code đổi không kéo theo reinstall vô ích.
  • Minimal — runtime không chứa compiler, test runner, package manager dư thừa nếu không cần.
  • Least privilege — chạy non-root, file ownership rõ.
  • No secrets — credential không nằm trong layer, history hoặc context.
  • Observable — command chính ghi log ra stdout/stderr và nhận signal đúng.

Thu nhỏ image — base gọn & ôn multi-stage

Kích thước image không phải chuyện “đẹp” — đó là độ trễ và chi phí: Image nhỏ pull nhanh hơn trên mọi node, khởi động nhanh hơn, và ít package hơn để quét CVE.

Chọn base nhỏ

Kiểu baseVí dụĐánh đổi
Fullnode:24, python:3.13Dễ debug; bề mặt tấn công lớn nhất
Slim / Alpinenode:24-alpine, python:3.13-slimNhỏ hơn nhiều; Alpine dùng musl — test dependency native
Distrolessgcr.io/distroless/nodejs24-debian12Không shell, OS tối thiểu — tốt cho runtime production

Ôn multi-stage (Phần 2): compiler, test runner và devDependenciesbuilder; chỉ artifact vào FROM cuối.

  BEFORE (single-stage, node:24)          AFTER (multi-stage + alpine runtime)
 ┌────────────────────────────┐          ┌────────────────────────────┐
 │ node:24 + npm ci (all deps)│          │ builder: compile / test    │
 │ + source + build tools     │   ──►    │         │ COPY --from      │
 │ ~900 MB typical            │          │ runtime: alpine + artifact │
 └────────────────────────────┘          │ ~80–120 MB typical         │
                                         └────────────────────────────┘

Dọn cache package trong cùng layer RUN — nếu không, file đã xóa vẫn nằm ở layer trước:

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

Xem Phần 2 cho ví dụ Node multi-stage đầy đủ; cùng mẫu với Go (builder → COPY --from → distroless).

So sánh kích thước sau build:

docker build -t myapp:fat -f Dockerfile.single .
docker build -t myapp:slim -f Dockerfile.multistage .
docker images myapp --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

Kỷ luật layer-cache — thứ tự cho tốc độ

Docker rebuild từ layer đổi đầu tiên trở xuống. Đặt chỉ thị đổi ít nhất lên trên và nhiều nhất xuống dưới:

  GOOD ORDER (fast rebuilds on code edits)
 ┌─────────────────────────────┐
 │ FROM (pinned tag/digest)    │  ← rarely changes
 ├─────────────────────────────┤
 │ RUN apt / system deps       │
 ├─────────────────────────────┤
 │ COPY package.json lockfile  │
 │ RUN npm ci                  │  ← cache survives code edits
 ├─────────────────────────────┤
 │ COPY . .                    │  ← changes every commit
 ├─────────────────────────────┤
 │ RUN npm run build           │
 │ CMD ...                     │
 └─────────────────────────────┘

Quy tắc mở rộng Phần 2:

  1. Copy manifest dependency trước source.
  2. Gộp RUN liên quan — một layer tốt hơn năm layer cài từng package.
  3. Đừng phá cache sớm — tránh COPY . . trước RUN npm ci.
  4. Chọn base có chủ đích — tag major như node:24-alpine dễ đọc; release cần digest nếu yêu cầu artifact bất biến.
docker build -t myapp:1.0 .
docker builder prune    # reclaim stale build cache when disk fills up

Chạy không phải root

Mặc định, tiến trình trong container chạy root (UID 0). Root trong container vẫn là root trong namespace đó — cấu hình sai, lỗi kernel và mount volume nhạy cảm trên host nguy hiểm hơn.

Tạo user riêng và chuyển trước CMD:

FROM node:24-alpine
WORKDIR /app
RUN addgroup -g 1001 -S app && adduser -u 1001 -S app -G app
COPY --chown=app:app package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=app:app . .
USER app
EXPOSE 3000
CMD ["node", "server.js"]
  • USER đặt UID/GID mặc định cho CMD, ENTRYPOINTdocker exec.
  • COPY --chown tránh file thuộc root mà user non-root không ghi được.

Kiểm tra lúc chạy:

docker run --rm myapp:1.0 whoami    # should print: app
docker exec -it <container> id

Image distroless thường có user nonroot sẵn — xem tài liệu image.


Secret đúng cách — không nhúng vào layer

Không bao giờ đặt secret trong ENV hoặc ARG của Dockerfile cho credential production. Layer image là lịch sử bất biến — ai có image có thể docker history hoặc trích layer.

Lộ secret — chứng minh

# BAD — do not ship this pattern
FROM alpine:3.24
ARG API_KEY=super-secret-key-demo
ENV API_KEY=${API_KEY}
RUN echo "configured" > /tmp/ok
docker build -t leaky:demo .
docker history leaky:demo --no-trunc | grep -i API_KEY
# or inspect build args in layer metadata

Giá trị có thể lộ trong lệnh layer và log build.

Cách sửa: -e hoặc Compose environment lúc chạy; secrets dưới /run/secrets/; BuildKit --mount=type=secret cho token chỉ lúc build — không commit vào layer.

Ví dụ BuildKit:

# syntax=docker/dockerfile:1
FROM node:24-alpine
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --omit=dev
docker build \
  --secret id=npmrc,src=$HOME/.npmrc \
  -t myapp:1.0 .

Lúc chạy, ưu tiên secret manager/orchestrator mount secret thành file ngắn hạn; env tiện nhưng vẫn có thể lộ qua inspect, crash dump hoặc log ứng dụng. Dù dùng kênh nào, không rebuild image với ENV PASSWORD=....


.dockerignore cho an toàn & tốc độ

Context build là mọi file gửi lên daemon (trừ ignore). Không có .dockerignore, bạn có thể copy .env, .git, node_modules host vào layer — build chậm và lộ nhầm.

# .dockerignore — typical Node / Python project
.git
.gitignore
.env
.env.*
*.pem
*.key
node_modules
dist
build
coverage
.vscode
.idea
**/*.md
Dockerfile*
.dockerignore

Theo dõi kích thước context khi build:

docker build -t myapp:1.0 . 2>&1 | head -5
# "transferring context: 12.34kB" vs hundreds of MB without ignore

Quét image — tìm CVE trước prod

Image nhỏ giúp ích, nhưng lỗ hổng nằm trong package. Quét mỗi bản release candidate.

Docker Scout:

docker scout quickview node:24-alpine
docker scout cves myapp:1.0

Trivy (phổ biến trong CI):

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image myapp:1.0

Thói quen cứng hóa:

  • Dùng tag version để con người đọc được và digest khi release cần tham chiếu bất biến.
  • Rebuild định kỳ — bản vá base lên Hub; image app không tự khỏe.
docker pull node:24-alpine
docker build --pull -t myapp:1.0 .

Các bẫy thường gặp

  • Tag latest trôi — prod cần version hoặc digest rõ.
  • Secret trong ARG/ENV — còn trong history.
  • Image một layer khổng lồ — một RUN không dọn cache, hoặc copy cả repo trước deps.
  • Chạy root mặc định — thêm USER; sửa quyền bằng --chown.
  • Không ghim version — build không tái lập và CVE bất ngờ.
  • Bỏ qua quét — đẩy :1.0 không scan → gặp CVE trên production.

Bảng tra nhanh

# size & layers
docker images myapp --format "table {{.Tag}}\t{{.Size}}"
docker history myapp:1.0
docker image inspect myapp:1.0 --format '{{.Config.User}}'

# build slim + cache-friendly
docker build --pull -t myapp:1.0 .
docker build --no-cache -t myapp:1.0 .

# non-root check
docker run --rm myapp:1.0 whoami
docker run --rm myapp:1.0 id

# secrets at build (BuildKit)
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp:1.0 .

# scan
docker scout cves myapp:1.0
# trivy scans a local Engine image through the Docker socket
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image myapp:1.0

# cleanup
docker builder prune

Bài tập / Exercises

Tạo docker-lab-07/ và làm bài trong đó. Mỗi bài chạy độc lập.

1. Build image fat single-stage và slim multi-stage; so sánh kích thước docker images.

Lời giải

Fat: node:24 + copy hết trước; slim: alpine, dependency trước source, USER node (giống multi-stage Phần 2).

docker build -f Dockerfile.fat -t lab07:fat .
docker build -f Dockerfile.slim -t lab07:slim .
docker images lab07 --format "table {{.Tag}}\t{{.Size}}"

2. Thêm USER non-root, chạy container, xác nhận bằng docker exec ... whoami.

Lời giải
docker run -d --name lab07-web -p 3070:3000 lab07:slim
docker exec lab07-web whoami    # node
docker exec lab07-web id        # uid=1000(node) ...
docker stop lab07-web && docker rm lab07-web

3. Build demo leaky, chứng minh secret trong docker history, rồi build safe chỉ dùng -e lúc chạy.

Lời giải
# leaky: ARG + ENV API_KEY=... in Dockerfile (see section above)
docker build -f Dockerfile.leaky -t lab07:leaky .
docker history lab07:leaky --no-trunc | grep -i API_KEY

# safe: no secret in image; inject at run time
docker build -f Dockerfile.safe -t lab07:safe .
docker run --rm -e API_KEY=demo-leaked-key-12345 lab07:safe

4. Thêm .dockerignore loại node_modules, .env, .git; build lại và xem dòng transferring context nhỏ hơn.

Lời giải
node_modules
.git
.env
.env.*
docker build -t lab07:slim .
# compare "transferring context" before/after adding .dockerignore

5. Quét lab07:slim bằng docker scout cves hoặc Trivy; liệt kê ít nhất một finding hoặc xác nhận sạch.

Lời giải
docker scout cves lab07:slim
# or:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image lab07:slim

Nếu không có Scout, dùng Trivy hoặc UI scan của Docker Desktop.

Nâng cao: Ghim base bằng digest, rebuild --pull, ghi lại một CVE được sửa khi cập nhật base.


Điểm chính

  • Base nhỏ + multi-stage chỉ đưa phần chạy prod; dọn cache trong cùng RUN.
  • Kỷ luật cache — dependency trước source, chỉ thị ít đổi lên trên.
  • USER non-root giới hạn thiệt hại; COPY --chown cho file app cần ghi.
  • Secret ở runtime hoặc BuildKit mount — không trong layer ENV/ARG commit.
  • .dockerignore + quét + digest cho release giữ build nhanh, artifact bất biến và CVE lộ trước khi deploy.

Tiếp theo

Phần 8 — Debugging & Troubleshooting Dockerdocker logs, exec, inspect, exit code, bẫy mạng/volume và checklist debug khi container hành xử lạ.