Dockerfile Best Practices & Multi-Stage Builds

Chào mừng đến Deep Dive đầu tiên trong series Docker & Kubernetes! Ở Phase 2 bạn đã học viết Dockerfiles và dùng Docker Compose. Giờ là lúc viết Dockerfiles production-ready — nhỏ, bảo mật, build nhanh và dễ bảo trì.
Dockerfile viết tệ có thể tạo ra images 1GB+, build mất mấy phút, chạy bằng root và lộ secrets. Dockerfile viết tốt tạo images dưới 50MB, build trong vài giây (nhờ caching), chạy bằng non-root user và chỉ chứa những gì cần để chạy app.
Bạn Sẽ Học Được
✅ Giảm image size 70-95% với base images phù hợp
✅ Thành thạo layer caching cho builds cực nhanh
✅ Dùng multi-stage builds để tách build-time và runtime dependencies
✅ Hardening images với non-root users và read-only filesystems
✅ Viết Dockerfiles tối ưu cho Node.js, Python, Go, Java và Rust
✅ Tận dụng BuildKit features như cache mounts và build secrets
✅ Scan images tìm vulnerabilities và debug build issues
✅ Tích hợp Docker builds vào CI/CD pipelines
Thời gian: 3–5 ngày, 1–2 giờ mỗi ngày
Yêu cầu: Giai đoạn 2: Docker Compose & Ứng Dụng Multi-Container
Phần 1: Tối Ưu Image Size
Tại Sao Image Size Quan Trọng
Mỗi megabyte trong Docker image đều có hệ quả:
| Tác động | Image nhỏ (50MB) | Image lớn (1.2GB) |
|---|---|---|
| Pull time | ~2 giây | ~45 giây |
| Storage | 50MB × 100 nodes = 5GB | 1.2GB × 100 nodes = 120GB |
| Attack surface | Ít packages = ít CVEs | Full OS = hàng trăm CVEs |
| Startup time | Vài giây (ít phải download) | Vài phút khi cold start |
| CI/CD costs | Pipeline nhanh, ít bandwidth | Pipeline chậm, egress đắt |
Chọn Base Image Phù Hợp
Base image là yếu tố lớn nhất ảnh hưởng đến image size cuối cùng:
| Base Image | Size | Shell | Package Manager | Phù hợp cho |
|---|---|---|---|---|
ubuntu / debian | 77-130MB | Có | apt | Development, debugging |
*-slim | 50-130MB | Có | apt (tối thiểu) | Production (khi cần shell) |
*-alpine | 5-50MB | Có | apk | Production (footprint nhỏ) |
distroless | 2-50MB | Không | Không | Production (bảo mật tối đa) |
scratch | 0MB | Không | Không | Binary static (Go, Rust) |
Lưu ý Alpine: Alpine dùng musl libc thay vì glibc. Điều này có thể gây vấn đề tương thích với một số libraries. Nếu gặp segfaults hoặc performance issues lạ, thử slim thay thế.
File .dockerignore
Trước khi build, Docker gửi toàn bộ build context đến daemon. Không có .dockerignore, nó gửi mọi thứ — bao gồm node_modules, .git và test files.
# Version control
.git
.gitignore
# Dependencies (sẽ được cài mới)
node_modules
vendor
__pycache__
*.pyc
# Build artifacts
dist
build
target
*.o
*.exe
# Development files
.env
.env.*
*.md
LICENSE
docker-compose*.yml
Dockerfile*
# IDE
.vscode
.idea
*.swp
# Tests (không cần trong production image)
tests
test
__tests__
*.test.js
*.spec.js
coverage
.nyc_outputTác động: Một Node.js project có node_modules có thể có build context 500MB+. Với .dockerignore, giảm xuống vài MB.
Dọn Sạch Package Manager Caches
Mỗi RUN instruction tạo layer mới. Cài đặt và dọn dẹp trong cùng layer:
# ❌ TỆ: Cache nằm lại trong layer
RUN apt-get update
RUN apt-get install -y curl wget
RUN rm -rf /var/lib/apt/lists/*
# ✅ TỐT: Cài và dọn trong một layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget \
&& rm -rf /var/lib/apt/lists/*# ❌ TỆ: pip cache nằm lại trong layer
RUN pip install -r requirements.txt
# ✅ TỐT: Không cache
RUN pip install --no-cache-dir -r requirements.txt# ❌ TỆ: apk cache nằm lại
RUN apk add curl
# ✅ TỐT: Không cache
RUN apk add --no-cache curlPhần 2: Layer Caching
Docker build images từng layer. Nếu layer không thay đổi, Docker dùng lại cached version. Hiểu điều này là chìa khóa cho builds nhanh.
Layer Caching Hoạt Động Thế Nào
Quy tắc: Khi một layer thay đổi, tất cả layers sau đó bị invalidated.
Trong ví dụ trên, nếu bạn thay đổi application code nhưng không thay package.json, Docker dùng lại cached npm ci layer (có thể mất 30+ giây). Chỉ COPY . . và npm run build chạy lại.
Thứ Tự Instructions
Nguyên tắc vàng: Copy files ít thay đổi nhất trước.
# ✅ TỐT: Dependencies ít thay đổi, source code thay đổi thường xuyên
FROM node:20-alpine
WORKDIR /app
# Layer 1: Dependencies (thay đổi hiếm khi)
COPY package.json package-lock.json ./
RUN npm ci --production
# Layer 2: Source code (thay đổi thường xuyên)
COPY . .
CMD ["node", "server.js"]# ❌ TỆ: Bất kỳ thay đổi source code nào đều invalidate npm install cache
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --production
CMD ["node", "server.js"]Tách Dev và Prod Dependencies
FROM node:20-alpine
WORKDIR /app
# Chỉ cài production dependencies
COPY package.json package-lock.json ./
RUN npm ci --production
# Copy source code
COPY . .
CMD ["node", "server.js"]Cho Python:
FROM python:3.12-slim
WORKDIR /app
# Cài dependencies trước
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]Cache Busting
Đôi khi bạn cần ép một layer rebuild. Kỹ thuật phổ biến — dùng build arguments:
FROM ubuntu:22.04
# Arg này thay đổi cache key — tăng lên để ép lấy packages mới
ARG CACHE_BUST=1
RUN apt-get update && apt-get install -y curl# Ép rebuild layer này
docker build --build-arg CACHE_BUST=$(date +%s) .Phần 3: Multi-Stage Builds
Multi-stage builds là kỹ thuật Dockerfile mạnh nhất. Chúng cho phép bạn dùng một image để build và một image hoàn toàn khác (nhỏ hơn) để chạy.
Vấn Đề
Build image điển hình chứa compilers, build tools, dev dependencies — những thứ bạn không cần lúc runtime.
# ❌ Single-stage: Image 1.2GB chứa cả build tools
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
# Image chứa: Node.js, npm, node_modules (dev + prod), source code, build toolsGiải Pháp: Multi-Stage
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
# Chỉ copy những gì cần từ build stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json /app/package-lock.json ./
RUN npm ci --production
# Bảo mật: chạy bằng non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]Kết quả: Build stage có mọi thứ cần để compile. Production stage chỉ có compiled output và production dependencies.
Named Stages và Targets
Bạn có thể có nhiều stages và build cụ thể từng cái:
# Base stage: shared dependencies
FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
# Development stage
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Build stage
FROM base AS builder
RUN npm ci
COPY . .
RUN npm run build
# Test stage
FROM builder AS test
RUN npm run test
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json /app/package-lock.json ./
RUN npm ci --production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
CMD ["node", "dist/server.js"]Build specific targets:
# Build chỉ dev image
docker build --target development -t my-app:dev .
# Build chỉ test stage (chạy tests trong lúc build)
docker build --target test -t my-app:test .
# Build production image (mặc định — stage cuối)
docker build -t my-app:prod .Copy từ External Images
Bạn có thể copy files từ bất kỳ image nào, không chỉ build stages:
FROM alpine:3.19
# Copy binary từ image khác
COPY --from=golang:1.22-alpine /usr/local/go/bin/go /usr/local/bin/go
# Copy nginx config từ official nginx image
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.confPhần 4: Security Hardening
Chạy containers với default settings là rủi ro bảo mật. Đây là các kỹ thuật hardening cần thiết.
Chạy Bằng Non-Root
Mặc định, containers chạy bằng root. Nếu attacker escape khỏi container, họ là root trên host.
# ❌ TỆ: Chạy bằng root (mặc định)
FROM node:20-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
# ✅ TỐT: Tạo và dùng non-root user
FROM node:20-alpine
WORKDIR /app
# Tạo system group và user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy files và set ownership
COPY --chown=appuser:appgroup . .
# Chuyển sang non-root user
USER appuser
CMD ["node", "server.js"]Cho images Debian/Ubuntu:
RUN groupadd -r appgroup && useradd -r -g appgroup -d /app -s /sbin/nologin appuserRead-Only Filesystem
Ngăn ghi vào container filesystem — mọi thay đổi nên đi vào mounted volumes:
# Chạy với read-only filesystem
docker run --read-only --tmpfs /tmp my-app:latestTrong Kubernetes:
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000Giảm Thiểu Attack Surface
# ✅ Loại bỏ packages không cần sau build
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y gcc libpq-dev
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
# Chỉ copy installed packages — không gcc, không build tools
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
USER 1000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]Scan Vulnerabilities
Scan images trước khi push lên production:
# Trivy — scanner mã nguồn mở phổ biến
trivy image my-app:latest
# Docker Scout (có sẵn trong Docker Desktop)
docker scout cves my-app:latest
# Snyk
snyk container test my-app:latest
# Grype
grype my-app:latestVí dụ output Trivy:
my-app:latest (alpine 3.19)
============================
Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 1, HIGH: 1, CRITICAL: 0)
┌───────────────┬──────────────────┬──────────┬─────────────┐
│ Library │ Vulnerability │ Severity │ Version │
├───────────────┼──────────────────┼──────────┼─────────────┤
│ libcrypto3 │ CVE-2024-XXXXX │ HIGH │ 3.1.4-r1 │
│ libssl3 │ CVE-2024-XXXXX │ MEDIUM │ 3.1.4-r1 │
│ busybox │ CVE-2024-XXXXX │ LOW │ 1.36.1-r15 │
└───────────────┴──────────────────┴──────────┴─────────────┘Đừng Lộ Secrets
# ❌ TỆ: Secret hiển thị trong image history
FROM node:20-alpine
ENV API_KEY=sk-12345
COPY . .
CMD ["node", "server.js"]
# ❌ TỆ: Secret trong layer (dù xóa sau đó)
COPY .env .
RUN source .env && npm run build
RUN rm .env # Vẫn còn trong layer trước!
# ✅ TỐT: Dùng BuildKit secrets (không bao giờ lưu trong image)
# syntax=docker/dockerfile:1
FROM node:20-alpine
RUN --mount=type=secret,id=api_key \
API_KEY=$(cat /run/secrets/api_key) npm run build# Truyền secret lúc build (không bao giờ lưu trong image layers)
docker build --secret id=api_key,src=./api_key.txt .Phần 5: Best Practices Theo Ngôn Ngữ
Node.js
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Dùng npm ci cho builds tái tạo được (tuân thủ package-lock.json chính xác)
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production
FROM node:20-alpine
WORKDIR /app
# Dùng tini làm PID 1 để xử lý signal đúng cách
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
# Chỉ copy production artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]Điểm chính:
- Dùng
npm ci(không phảinpm install) cho deterministic builds npm prune --productionloại bỏ devDependencies- Tini xử lý SIGTERM đúng cách (graceful shutdown)
- Image cuối: ~80MB thay vì ~1GB
Python
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
# Cài build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
# Cài Python dependencies vào custom prefix
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
# Chỉ copy installed packages từ builder
COPY --from=builder /install /usr/local
# Copy application code
COPY . .
# Non-root user
RUN useradd -r -s /sbin/nologin appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Điểm chính:
- Build dependencies (gcc) chỉ trong builder stage
--prefix=/installđặt packages vào directory sạch để copy- Image cuối không có compiler, không build tools — chỉ Python + packages
- Image cuối: ~150MB thay vì ~1GB
Go
Go tạo binary static — hoàn hảo cho minimal images:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies trước (cache-friendly)
COPY go.mod go.sum ./
RUN go mod download
# Build binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# Image cuối: scratch = 0MB base
FROM scratch
# Copy SSL certificates (cần cho HTTPS calls)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy binary
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]Điểm chính:
CGO_ENABLED=0tạo binary hoàn toàn static-ldflags="-s -w"strip debug info (binary nhỏ hơn)scratchlà image rỗng — chỉ có binary của bạn- Image cuối: ~5-15MB thay vì ~300MB
- Không shell, không package manager, không attack surface
Java (Spring Boot)
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# Copy Gradle/Maven files trước cho caching
COPY build.gradle settings.gradle gradlew ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon
# Build ứng dụng
COPY src ./src
RUN ./gradlew bootJar --no-daemon
# Extract layers cho caching tốt hơn
RUN java -Djarmode=layertools -jar build/libs/*.jar extract
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Copy Spring Boot layers (từ ít đến nhiều thay đổi nhất)
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Điểm chính:
- Dùng JRE (không phải JDK) trong production — không cần compiler
- Spring Boot layer extraction cho Docker caching tốt hơn
- Dependencies ít thay đổi → cached layer
- Application code thay đổi thường → chỉ layer trên cùng rebuild
- Image cuối: ~200MB thay vì ~500MB
Rust
# syntax=docker/dockerfile:1
FROM rust:1.77-alpine AS builder
WORKDIR /app
# Cài musl target cho static linking
RUN apk add --no-cache musl-dev
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release --target=x86_64-unknown-linux-musl
RUN rm -rf src
# Build ứng dụng thật
COPY src ./src
RUN touch src/main.rs && cargo build --release --target=x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/myapp"]Điểm chính:
- musl target tạo binary hoàn toàn static (không phụ thuộc libc)
- Trick dummy
main.rscache dependency compilation scratchbase — chỉ có binary- Image cuối: ~5-10MB
Phần 6: BuildKit Features
BuildKit là Docker build engine hiện đại. Nhanh hơn, linh hoạt hơn và có features không có trong legacy builder.
Bật BuildKit
# Cách 1: Environment variable
export DOCKER_BUILDKIT=1
docker build .
# Cách 2: Dùng docker buildx (BuildKit luôn được bật)
docker buildx build .
# Cách 3: Đặt mặc định trong Docker daemon config
# /etc/docker/daemon.json
{
"features": { "buildkit": true }
}Lưu ý: Docker Desktop đã bật BuildKit mặc định từ Docker 23.0+.
Cache Mounts
Cache mounts giữ lại directory giữa các lần build — hoàn hảo cho package manager caches:
# syntax=docker/dockerfile:1
# Node.js: Cache npm packages
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
# Python: Cache pip packages
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
# Go: Cache module downloads và build cache
FROM golang:1.22
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app/server .Build Secrets
Truyền secrets lúc build mà không lưu trong layers:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
# Mount secret trong lúc build — không lưu trong image
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
COPY . .
CMD ["node", "server.js"]# Build với secret
docker build --secret id=npmrc,src=$HOME/.npmrc .Multi-Platform Builds
Build images cho nhiều architectures (x86, ARM):
# Tạo multi-platform builder
docker buildx create --name multiplatform --use
# Build cho nhiều platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry/my-app:latest \
--push .Điều này đặc biệt quan trọng cho:
- Developers Apple Silicon (M1/M2/M3) deploy lên x86 servers
- Hỗ trợ cả x86 và ARM cloud instances (ARM rẻ hơn trên AWS)
- Edge/IoT devices chạy ARM
Phần 7: Tích Hợp CI/CD
Ví Dụ GitHub Actions
name: Build and Push Docker Image
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64Features chính:
cache-from/cache-to: type=gha— Dùng GitHub Actions cache cho Docker layers- Multi-platform build (amd64 + arm64)
- Chỉ push trên main branch (không trên PRs)
- Tag với cả
latestvà commit SHA
Chiến Lược Image Tagging
# Semantic versioning
my-app:1.2.3
my-app:1.2
my-app:1
# Dựa trên Git
my-app:abc123def # commit SHA
my-app:main # branch name
my-app:pr-42 # pull request number
# Dựa trên ngày
my-app:2024-01-20
my-app:20240120-abc123
# Kết hợp (khuyến nghị)
my-app:1.2.3-abc123Best practice: Đừng chỉ dựa vào latest. Luôn tag với version cụ thể hoặc commit SHA để trace chính xác code nào đang chạy.
Phần 8: Debug Dockerfiles
Kiểm Tra Image Layers
# Xem layer history và sizes
docker history my-app:latest
# Ví dụ output
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5 2 hours ago CMD ["node" "server.js"] 0B
f6g7h8i9j0 2 hours ago EXPOSE map[3000/tcp:{}] 0B
k1l2m3n4o5 2 hours ago COPY . . # buildkit 45.2kB
p6q7r8s9t0 2 hours ago RUN npm ci --production # buildkit 38.5MB
u1v2w3x4y5 2 hours ago COPY package*.json ./ # buildkit 1.2kB
z6a7b8c9d0 2 hours ago WORKDIR /app 0B
e1f2g3h4i5 3 weeks ago /bin/sh -c #(nop) CMD ["node"] 0BDùng Dive Phân Tích Layers
Dive là tool tuyệt vời để khám phá Docker image layers:
# Cài dive
brew install dive # macOS
apt-get install dive # Debian/Ubuntu
# Phân tích image
dive my-app:latestDive cho bạn thấy:
- Size và nội dung từng layer
- Wasted space (files thêm rồi xóa ở layers sau)
- Image efficiency score
Debug Build Failures
# BuildKit output chi tiết
docker build --progress=plain .
# Build đến một stage cụ thể
docker build --target builder -t debug-stage .
# Shell vào failed stage
docker run -it debug-stage /bin/sh
# Build không cache (bắt đầu từ đầu)
docker build --no-cache .Vấn Đề Thường Gặp và Cách Fix
| Vấn đề | Nguyên nhân | Fix |
|---|---|---|
| Image quá lớn | Base image sai hoặc cache chưa dọn | Dùng alpine/slim, dọn trong cùng RUN |
| Build chậm | Cache invalidation quá sớm | Copy dependency files trước source code |
npm install chạy mỗi lần | COPY . . trước npm install | Copy package*.json trước, rồi npm install |
| Permission denied lúc runtime | Files thuộc root, chạy bằng non-root | Dùng COPY --chown=user:group |
| Secrets trong image history | ENV hoặc COPY cho secrets | Dùng BuildKit --secret mount |
| Signal handling issues | App là PID 1 không có init | Dùng tini hoặc dumb-init làm entrypoint |
Bài Tập
Bài Tập 1: Tối Ưu Dockerfile Có Sẵn
Cải thiện Dockerfile chưa tối ưu này:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "index.js"]Mục tiêu:
- Giảm image size ít nhất 70%
- Dùng layer caching cho dependencies
- Chạy bằng non-root user
- Thêm file .dockerignore
Bài Tập 2: Ứng Dụng Go Multi-Stage
Viết Dockerfile cho Go web server:
- Compile thành static binary
- Dùng
scratchlàm base image cuối - Bao gồm SSL certificates cho HTTPS calls
- Image cuối nên dưới 15MB
Bài Tập 3: So Sánh Ngôn Ngữ
Build cùng "Hello World" HTTP server bằng ba ngôn ngữ (Node.js, Go, Python) và so sánh:
- Build time với cold cache
- Build time với warm cache
- Image size cuối cùng
- Số CVEs (dùng Trivy)
Tiếp Theo
Trong bài tiếp theo, chúng ta sẽ đào sâu vào Docker Networking & Volumes — hiểu cách containers giao tiếp và persist data:
- Kiến trúc Docker networking (bridge, host, overlay, macvlan)
- Giao tiếp container-to-container
- Storage drivers và union filesystems
- Volumes, bind mounts và tmpfs
- Production storage patterns
Tóm Tắt và Điểm Chính
✅ Chọn base image nhỏ nhất khả dụng: alpine > slim > full OS > scratch cho compiled languages
✅ Luôn dùng .dockerignore để loại node_modules, .git, tests và dev files khỏi build context
✅ Sắp xếp Dockerfile instructions theo tần suất thay đổi — dependencies trước, source code sau
✅ Multi-stage builds tách build-time tools khỏi production — giảm image size 70-95%
✅ Chạy containers bằng non-root users — đừng bao giờ chạy bằng root trong production
✅ Dùng BuildKit features: cache mounts cho package managers, secrets cho sensitive data
✅ Scan images với Trivy/Snyk/Grype trước khi deploy lên production
✅ Dùng npm ci (không phải npm install) cho Node.js builds tái tạo được
✅ Go và Rust có thể dùng scratch base images cho containers tối thiểu 5-15MB
✅ Tag images với versions cụ thể hoặc commit SHAs — đừng chỉ dựa vào latest
Series: Lộ Trình Học Docker & Kubernetes
Trước đó: Giai đoạn 3: Kubernetes Cơ Bản
Tiếp theo: Deep Dive: Docker Networking & Volumes
Có câu hỏi về Dockerfile optimization? Hãy liên hệ hoặc để lại comment!
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.