Bài 02
Image & Dockerfile: từ Docker Hub đến image của riêng mình
Bài 02 — Image & Dockerfile: từ Docker Hub đến image của riêng mình
Đây là bài 02 của chặng Docker. Bài 01 bạn đã docker run nginx được — dùng image của người khác trên Docker Hub. Câu hỏi đặt ra: khi nào cần build image của riêng mình?
Trả lời: gần như mọi project production thật. Bạn không deploy nginx:latest raw lên server — bạn deploy cty-web:v2.3.1 đã có sẵn code + config + dependency của bạn.
Mục tiêu bài này: hiểu image deeply (layers, tags, registry), viết được Dockerfile tốt, tối ưu image từ 2GB xuống 100MB.
Vấn đề: tại sao cần image riêng?
docker run nginx chỉ là server web rỗng. Production cần:
- Code app của bạn (PHP, Node.js, Python, Java...).
- Dependency cụ thể (PHP 8.2.5, không phải 8.1).
- Config custom (nginx.conf riêng, SSL cert, env var).
- Security hardening (non-root user, file permission).
Cách 1 — runtime config: mount volume code/config vào container nginx. Hoạt động được nhưng:
- Mỗi deploy phải copy file lên server.
- Setup phức tạp (volume mount, env vars).
- Khó share giữa môi trường (dev/staging/prod khác nhau).
Cách 2 — build image riêng: cty-web:v2.3.1 chứa mọi thứ sẵn sàng. Pull về là chạy. Đây là cách production làm.
→ Bài này dạy bạn cách 2.
Image là gì — đào sâu
Định nghĩa
Image là bản đóng gói của ứng dụng đang chạy, bao gồm app binary và dependency. Chứa metadata về việc khởi tạo container.
⚠️ Image KHÔNG phải là OS image. Đây là điểm nhầm lẫn lớn nhất:
- Image VMware ESXi = nguyên 1 OS Linux đầy đủ + kernel + app.
- Docker image = CHỈ chứa user-space cần để chạy app. Không có kernel. Dùng kernel của host.
ESXi VM image (.vmdk): Docker image (nginx):
┌──────────────┐ ┌──────────────┐
│ App │ │ nginx │
│ Libs │ │ Libs (libc) │
│ Bash/utils │ │ (chỉ files │
│ Init │ │ cần thiết) │
│ Kernel │ ← có kernel │ KHÔNG kernel│
└──────────────┘ └──────────────┘
~500MB-2GB ~25-100MBImage lưu ở đâu
Lưu trữ và chia sẻ image qua registry. Registry phổ biến:
- Docker Hub — public, default.
docker pull nginxlấy từ đây. - AWS ECR, GCR, Azure Container Registry — private cloud.
- Harbor, GitLab Registry, Nexus — self-hosted.
Cùng image có thể có nhiều tag (version): nginx:1.25, nginx:1.25-alpine, nginx:latest.
Image được tạo từ Dockerfile
┌──────────────┐ ┌──────────────┐
│ Dockerfile │ │ Code │
│ (text) │ │ app, html, │
│ │ │ config... │
└──────┬───────┘ └──────┬───────┘
│ │
└───────────────┬───────────────┘
│
▼
┌──────────────┐
│ docker build │ ← Trên máy dev/CI
│ -t │
│ name:tag . │
└──────┬───────┘
│
▼
┌──────────────┐
│ Image local │ (chỉ trên máy build)
│ (binary + │
│ metadata) │
└──────┬───────┘
│ docker push
▼
┌─────────────────────┐
│ REGISTRY │ (Docker Hub, ECR, ...)
│ ┌───────────────┐ │
│ │ myapp:v1.0 │ │ ← Public/private storage
│ │ myapp:v1.1 │ │
│ │ myapp:latest │ │
│ └───────────────┘ │
└──────┬───────────────┘
│
┌────────────┼────────────┐
│ docker │ docker │ docker pull
│ pull │ pull │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Server A│ │Server B│ │Server C│ ← Production servers
│ │ │ │ │ │
│ Image │ │ Image │ │ Image │ ← Cache local
│ cached │ │ cached │ │ cached │
│ │ │ │ │ │ │ │ │
│ ▼ │ │ ▼ │ │ ▼ │ docker run
│Container│ │Container│ │Container│
└────────┘ └────────┘ └────────┘Flow tổng: Dockerfile → build local → push registry → pull về nhiều server → run thành nhiều container.
→ Đây là pattern chuẩn của modern deployment: build 1 lần, deploy nhiều nơi.
Layer Architecture — tinh hoa của Docker
Image được tạo thành bởi nhiều layers. Đây là tech quan trọng nhất, hiểu rõ → tối ưu được mọi thứ.
Layer là gì
Mỗi instruction trong Dockerfile tạo 1 layer. Layer = diff so với layer trước.
Dockerfile: Layers (từ dưới lên):
┌─────────────────────┐
FROM ubuntu:22.04 │ Layer 1: ubuntu base│ ← 70 MB
├─────────────────────┤
RUN apt update │ Layer 2: apt update │ ← 30 MB
├─────────────────────┤
RUN apt install -y nginx │ Layer 3: nginx pkg │ ← 50 MB
├─────────────────────┤
COPY index.html /var/www/ │ Layer 4: file copy │ ← 1 KB
├─────────────────────┤
CMD ["nginx", "-g", "daemon off;"]│ Layer 5: CMD metadata│ ← 0 KB
└─────────────────────┘
Total: ~150 MBLayer Sharing — magic tiết kiệm storage
Mỗi layer chỉ được lưu 1 lần trên host. Nhiều image dùng chung layer → share storage.
Image A (web-app:v1): Image B (web-app:v2):
┌─────────────────────┐ ┌─────────────────────┐
│ App code v2.1 │ │ App code v2.2 │ ← khác (10MB)
├─────────────────────┤ ├─────────────────────┤
│ Node.js modules │ ←──────────┤ Node.js modules │ ← chung (50MB)
├─────────────────────┤ ├─────────────────────┤
│ Node.js runtime │ ←──────────┤ Node.js runtime │ ← chung (80MB)
├─────────────────────┤ ├─────────────────────┤
│ Alpine base │ ←──────────┤ Alpine base │ ← chung (5MB)
└─────────────────────┘ └─────────────────────┘
Disk: 145 MB Disk: 10 MB (chỉ layer khác)
Tổng disk thực: 145 + 10 = 155 MB (không phải 290 MB)→ Tiết kiệm bộ nhớ + tiết kiệm thời gian pull/push. Pull web-app:v2 chỉ tải layer mới (10MB), không tải lại các layer chung.
Container = 1 layer of image
Container thực ra là 1 layer writable trên top của image.
Container (running):
┌────────────────────────┐
│ Writable layer │ ← container layer, mọi thay đổi đi vào đây
│ (chỉ tồn tại khi │
│ container đang chạy) │
├────────────────────────┤
│ App code │ ← image layers (read-only)
├────────────────────────┤
│ Runtime │ ← (đã share giữa các container)
├────────────────────────┤
│ Base image │
└────────────────────────┘Khi container ghi file → ghi vào writable layer. Container die → writable layer mất → data mất. (Bài 03 sẽ dạy volume để giải quyết.)
Kiểm tra layers
# Liệt kê layers của image
docker image history nginx:latest
# IMAGE CREATED CREATED BY SIZE
# abc123 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" ... 0B
# def456 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQ 0B
# ...
# fed987 3 weeks ago /bin/sh -c #(nop) ADD file:... 72MB
# Inspect chi tiết
docker image inspect nginx:latestdocker image history cực hữu ích khi debug image to: thấy layer nào ngốn dung lượng.
Image Tag — versioning
Tag = version của image. Format: <image-name>:<tag>.
nginx → nginx:latest (mặc định)
nginx:1.25 → version 1.25
nginx:1.25-alpine → version 1.25 + base alpine
nginx:stable → stable channel
myrepo/myapp:v2.3.1 → image của repo "myrepo":latest không phải "phiên bản mới nhất"
⚠️ Đây là hiểu lầm phổ biến nhất. :latest chỉ là tag mặc định. Nếu image push lên không có tag, sẽ tự gán :latest. KHÔNG có nghĩa là "version mới nhất".
Ví dụ: maintainer có thể push nginx:1.25 mà không push nginx:latest. docker pull nginx (= :latest) sẽ lấy version cũ.
Production: luôn pin tag cụ thể (nginx:1.25.3), không dùng :latest. Lý do:
- Reproducible — build lại cùng tag = cùng image.
- Tránh surprise —
:latestcó thể đổi underneath. - Rollback dễ — biết version cũ là gì.
Đánh tag image
# Sau khi build image local, đánh tag để push
docker image tag myapp myrepo/myapp:v1.0
docker image tag myapp myrepo/myapp:latest
# 1 image có thể có nhiều tag
docker image ls
# REPOSITORY TAG IMAGE ID
# myapp latest abc123
# myrepo/myapp v1.0 abc123 ← cùng IMAGE ID
# myrepo/myapp latest abc123Tag system — visual
Quan trọng: hiểu 1 image (binary) ↔ N tag (label):
Image (cùng binary):
┌────────────────────┐
│ IMAGE ID: │
│ sha256:abc123.. │
│ │
│ ~150 MB data │
└─────────┬──────────┘
│
(cùng image, nhiều tên gọi)
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────────┐
│ myapp │ │myapp:v1.0│ │myapp:latest │
│ :latest │ │ │ │ │
└─────────┘ └──────────┘ └──────────────┘
Cả 3 tag trỏ vào CÙNG image binary.
docker rm myapp:latest → chỉ xóa tag, image vẫn còn nếu có tag khác.# Demo: tag không tạo image mới
docker images myrepo/myapp
# REPOSITORY TAG IMAGE ID SIZE
# myrepo/myapp v1.0 abc123 150MB
# myrepo/myapp latest abc123 150MB ← cùng ID
# myrepo/myapp stable abc123 150MB ← cùng ID
# Disk thực: 150MB (không phải 450MB)Docker Hub — registry public
Docker Hub là registry mặc định của Docker — docker pull không chỉ định registry sẽ lấy từ đây.
Account và Repository
- Đăng ký account ở hub.docker.com (free).
- Tạo repository (public hoặc private).
hub.docker.com:
yourname/myapp → public repo
yourname/myapp-private → private (free 1 cái, paid cho thêm)Push image lên Docker Hub
# 1. Login
docker login
# Username: yourname
# Password: ****
# 2. Đánh tag image với prefix là username
docker image tag myapp yourname/myapp:v1.0
# 3. Push
docker image push yourname/myapp:v1.0
# 4. Verify trên hub.docker.com → repository xuất hiệnPull về dùng từ máy khác
# Trên server khác
docker pull yourname/myapp:v1.0
docker run -d yourname/myapp:v1.0Xóa Image
# Xóa image (chỉ khi không container nào đang dùng)
docker rmi nginx:1.25
docker image rm nginx:1.25 # same thing
# Force xóa (kể cả container đang dùng)
docker rmi -f nginx:1.25
# Xóa tất cả image không dùng
docker image prune
# Xóa luôn cả image bị "dangling" (không tag)
docker image prune -a⚠️ prune -a xóa MỌI image không có container running. Dùng cẩn thận trên server có nhiều image.
Dockerfile — bản design của image
Dockerfile là file text chứa mô tả tuần tự để tạo ra image. Mỗi dòng instruction tạo 1 layer.
Dockerfile đơn giản
Lab: tạo image nginx serve trang HTML custom.
Bước 1: Tạo file index.html:
<!DOCTYPE html>
<html>
<head><title>My Site</title></head>
<body><h1>Hello from my Docker image!</h1></body>
</html>Bước 2: Tạo Dockerfile:
FROM nginx:1.25-alpine
WORKDIR /usr/share/nginx/html/
COPY index.html index.html
EXPOSE 80Bước 3: Build image:
docker image build -t mynginx:v1 .
# -t name:tag context (thư mục hiện tại)Bước 4: Run:
docker container run -d -p 8080:80 mynginx:v1
curl http://localhost:8080
# <h1>Hello from my Docker image!</h1>→ Tạo được image custom đầu tiên. Đơn giản 4 dòng Dockerfile.
Giải thích từng instruction
FROM nginx:1.25-alpine— base image. Mọi image bắt đầu từ đây.WORKDIR /usr/share/nginx/html/— đặt working directory. Mọi command sau chạy ở đây.COPY index.html index.html— copy file từ host (context) vào image.EXPOSE 80— document app sẽ lắng nghe port 80. Không thực sự mở port — chỉ là metadata.
⚠️ EXPOSE không tự động -p. Vẫn cần -p 8080:80 khi docker run.
Dockerfile Instructions — bảng đầy đủ
| Instruction | Ý nghĩa |
|---|---|
FROM |
Base image (ví dụ: ubuntu, alpine, node:20). Mọi Dockerfile bắt đầu với FROM. |
WORKDIR |
Đặt working directory trong container. Tạo nếu chưa có. |
COPY |
Copy file/folder từ host (context) vào image. |
ADD |
Tương tự COPY, nhưng hỗ trợ download URL và giải nén .tar.gz. Production nên dùng COPY, ADD cho cases đặc biệt. |
RUN |
Chạy command lúc build image (cài package, compile code). |
CMD |
Command mặc định khi container chạy. Có thể bị override bởi docker run myimage <override>. |
ENTRYPOINT |
Tương tự CMD nhưng không bị override (trừ khi dùng --entrypoint). |
EXPOSE |
Document port app lắng nghe (không thực sự mở port). |
ENV |
Set environment variable trong image. |
VOLUME |
Khai báo mount point — sẽ tạo anonymous volume khi run. |
ARG |
Build-time variable. Dùng với docker build --build-arg. |
LABEL |
Metadata cho image (vd: maintainer, version). |
USER |
Đặt user chạy command sau (security best practice). |
HEALTHCHECK |
Định nghĩa health check cho container. |
COPY vs ADD
# COPY — đơn giản, dùng cho file local
COPY src/ /app/src/
# ADD — extra features
ADD https://example.com/file.tar.gz /app/ # download từ URL
ADD archive.tar.gz /app/ # tự giải nén
# Best practice: dùng COPY trừ khi cần feature của ADD
COPY archive.tar.gz /app/
RUN tar -xzf /app/archive.tar.gz -C /app/ && rm /app/archive.tar.gzCMD vs ENTRYPOINT
# CMD: command mặc định, BỊ override
CMD ["nginx", "-g", "daemon off;"]
# Run như bình thường:
# docker run myimg → chạy: nginx -g 'daemon off;'
# Override:
# docker run myimg ls -la → chạy: ls -la (không chạy nginx)
# ENTRYPOINT: command cố định, KHÔNG bị override
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"] # arguments (có thể override)
# docker run myimg → chạy: nginx -g 'daemon off;'
# docker run myimg -v → chạy: nginx -v
# docker run --entrypoint /bin/sh myimg → override entrypoint→ ENTRYPOINT cho cố định command, CMD cho args mặc định. Combo này là pattern phổ biến.
Build Cache — tăng tốc build
Build image lần đầu mất 5 phút. Build lại không đổi gì → 5 giây. Magic này là build cache.
Cách Docker cache
Khi build, Docker check từng layer:
Instruction này có thay đổi không?
Files COPY/ADD vào có thay đổi không?
Nếu KHÔNG đổi → reuse layer cached
Nếu CÓ đổi → build lại layer này + MỌI layer sauBuild cache flow — visual
Dockerfile (5 instructions)
┌──────────────────────────────────────────────────┐
│ Layer 1: FROM ubuntu:22.04 │
│ Layer 2: RUN apt update │
│ Layer 3: COPY package.json . │
│ Layer 4: RUN npm install │
│ Layer 5: COPY . . │
│ Layer 6: CMD ["node", "app.js"] │
└──────────────────────────────────────────────────┘
Build LẦN 1 (chưa có cache):
─────────────────────────────────────
Layer 1: ✓ Build (30s) → cache_1
Layer 2: ✓ Build (45s) → cache_2
Layer 3: ✓ Build (1s) → cache_3
Layer 4: ✓ Build (180s) → cache_4
Layer 5: ✓ Build (2s) → cache_5
Layer 6: ✓ Build (0s) → cache_6
Tổng: 258s
Build LẦN 2 (không đổi gì):
─────────────────────────────────────
Layer 1: ⚡ Cache hit (0s)
Layer 2: ⚡ Cache hit (0s)
Layer 3: ⚡ Cache hit (0s)
Layer 4: ⚡ Cache hit (0s)
Layer 5: ⚡ Cache hit (0s)
Layer 6: ⚡ Cache hit (0s)
Tổng: 0.5s ← cực nhanh
Build LẦN 3 (đổi app.js):
─────────────────────────────────────
Layer 1: ⚡ Cache hit (0s)
Layer 2: ⚡ Cache hit (0s)
Layer 3: ⚡ Cache hit (0s) ← package.json không đổi
Layer 4: ⚡ Cache hit (0s) ← npm install KHÔNG chạy lại
Layer 5: ✗ INVALIDATED (2s) ← code đổi, rebuild
Layer 6: ✗ Rebuild (0s) ← layer sau bị invalidate
Tổng: 2s
↑ Đây là lý do COPY package.json TRƯỚC COPY . .
→ Code đổi không trigger npm install (180s)Ví dụ cache invalidation
FROM ubuntu:22.04 # Layer 1 (cached)
RUN apt update && apt install -y nginx # Layer 2 (cached)
COPY config.txt /etc/config.txt # Layer 3 — nếu config.txt đổi → rebuild
COPY index.html /var/www/ # Layer 4 → rebuild (vì layer 3 đổi)
CMD ["nginx"] # Layer 5 → rebuild⚠️ Đổi 1 file ở Layer 3 → invalidate cache cho TẤT CẢ layer sau (4, 5). Layer 1, 2 vẫn cached.
→ Bài học: đặt instruction ÍT đổi ở trên, HAY đổi ở dưới.
Tối ưu Dockerfile — 9 best practices
Đây là phần quan trọng nhất của bài. Image tối ưu = build nhanh + pull nhanh + chạy nhanh + bảo mật.
1. Dùng base image nhỏ gọn
Đừng dùng ubuntu nếu không cần. So sánh base image:
| Image | Size | Use case |
|---|---|---|
ubuntu:22.04 |
~70 MB | Cần Ubuntu specifically |
debian:slim |
~30 MB | Cần Debian |
alpine:3.18 |
~5 MB | Đa số case |
scratch |
0 MB | Static binary (Go, Rust) |
# ❌ Tệ: ubuntu base
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
COPY app.py /app/
CMD ["python3", "/app/app.py"]
# → Image ~150 MB
# ✅ Tốt: alpine base
FROM python:3.11-alpine
COPY app.py /app/
CMD ["python", "/app/app.py"]
# → Image ~50 MB⚠️ Alpine pitfall: Alpine dùng musl libc thay vì glibc. Một số binary biên dịch cho glibc không chạy được trên Alpine. Test kỹ trước khi switch.
2. Multi-Stage Build — giảm kích thước drastically
Tách stage build (compile code) và stage runtime (chỉ chạy app).
# Stage 1: Build
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp # compile binary
# Stage 2: Runtime (chỉ giữ binary)
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]Without multi-stage: With multi-stage:
┌────────────────────┐ ┌────────────────────┐
│ Go compiler │ │ Final image: │
│ Go modules │ │ alpine:latest │
│ Source code │ │ + myapp binary │
│ Build artifacts │ │ (5MB + 10MB) │
│ Final binary │ └────────────────────┘
│ (800MB total) │
└────────────────────┘
Final: 800 MB Final: 15 MB→ Giảm 50-100 lần dung lượng image cho compiled language (Go, Rust, C++, Java). Production must.
3. Giảm số layer — RUN gộp commands
Mỗi RUN tạo 1 layer. Gộp lại bằng && để giảm layer.
# ❌ Tệ: 4 layer, mỗi RUN cache invalidation riêng
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean
# ✅ Tốt: 1 layer
RUN apt-get update && \
apt-get install -y curl vim && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*Cleanup phải cùng layer với install — vì layer khác cleanup không xóa được file của layer trước (chỉ "hide" về mặt FS, vẫn ngốn dung lượng).
4. .dockerignore — loại trừ file không cần
Khi docker build ., toàn bộ thư mục hiện tại (context) gửi lên Docker daemon. File không cần (.git, node_modules, log) → context to → build chậm.
Tạo .dockerignore ở cùng folder Dockerfile:
# .dockerignore
.git
.gitignore
node_modules
*.log
.env
README.md
tests/
.vscode→ Context giảm từ 500MB → 5MB. Build nhanh hơn 10 lần.
5. Không chạy với root — security
Mặc định container chạy với root — bug app có thể leverage thành root attack trên host.
# Tạo user thường
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Chuyển sang user đó
USER appuser
# Mọi command sau chạy với appuser
COPY --chown=appuser:appgroup . /app
WORKDIR /app
CMD ["./myapp"]⚠️ Một số app cần root (vd: bind port < 1024). Solution: bind port > 1024 trong container, map -p 80:8080.
6. Cache hiệu quả — COPY trước RUN
Đặt instruction ÍT đổi ở trên, HAY đổi ở dưới.
# ❌ Tệ: code đổi → invalidate cài deps
FROM node:20-alpine
WORKDIR /app
COPY . . # Layer thay đổi mỗi lần code update
RUN npm install # Cài lại deps mỗi build → chậm
# ✅ Tốt: deps cached, code đổi không invalidate
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./ # Layer này chỉ đổi khi deps đổi
RUN npm install # Cached nếu package.json không đổi
COPY . . # Code đổi → chỉ rebuild layer này→ Build từ 5 phút xuống 30 giây cho code change typical.
7. CMD và ENTRYPOINT đúng cách
# Dùng JSON form (exec form) — không qua shell
CMD ["nginx", "-g", "daemon off;"] # ✅ Tốt
CMD nginx -g "daemon off;" # ❌ Tệ (shell form, có quirks)Shell form chạy qua /bin/sh -c, có vấn đề:
- Signal handling tệ (SIGTERM không truyền cho app).
- Cần
tiniđể xử lý PID 1 properly.
→ Luôn dùng JSON/exec form.
8. Không lưu data trong image — Dùng Volume
Container là ephemeral (tạm thời). Container die = writable layer mất = data mất.
❌ Sai: lưu data trong image/container
Dockerfile:
FROM mysql:8
COPY mydata.sql /var/lib/mysql/ ← embed data vào image
Vấn đề:
- Image to ra (chứa data)
- Container die → restart container mới → data mất sạch
- Update image → data cũ không carry sang✅ Đúng: tách data ra Volume
Image: FROM mysql:8 (sạch, chỉ chứa binary)
│
│ docker run -v db-data:/var/lib/mysql
▼
Container chạy
│
│ ghi data
▼
┌─────────────────┐
│ Volume │ ← Data PERSISTENT trên host
│ "db-data" │ (sống lâu hơn container)
│ (host disk) │
└─────────────────┘
Container die → volume vẫn còn
Tạo container mới → mount lại volume → data trở vềBài 03 (Network & Storage) sẽ đào sâu về Volume, Bind Mount, tmpfs. Tạm thời nhớ nguyên tắc:
- Image = code + dependency (tĩnh, immutable).
- Volume = data động (mutable, persistent).
⚠️ Khi Dockerfile có VOLUME /path → mỗi container chạy sẽ tự tạo anonymous volume cho path đó. Đây là hint cho người dùng "đây là chỗ chứa data, nhớ mount volume".
# Ví dụ: image database
FROM postgres:15-alpine
# Hint: chỗ này nên mount volume
VOLUME ["/var/lib/postgresql/data"]
EXPOSE 54329. Pin version base image
# ❌ Tệ: latest → image đổi underneath
FROM node:latest
FROM nginx
# ✅ Tốt: specific version
FROM node:20.4-alpine
FROM nginx:1.25.3-alpine
# Tốt hơn: digest cho immutability
FROM nginx@sha256:abc123...Digest là hash SHA256 của image — không bao giờ đổi. Production critical dùng digest.
10. Dọn dẹp file tạm trong cùng layer
# ❌ Tệ: cache apt ở layer khác → vẫn ngốn dung lượng
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ Tốt: cleanup cùng layer
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/* && \
apt-get cleanFile trong layer trước, dù bị "xóa" ở layer sau, vẫn nằm trong image (chỉ bị hide). Cleanup phải cùng layer.
Dockerfile production-grade — ví dụ thực
Combine tất cả best practice:
# ─── Stage 1: Build ────────────────────────────────
FROM node:20.4-alpine AS builder
WORKDIR /app
# Copy package files trước, cache deps
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Build app
RUN npm run build
# ─── Stage 2: Runtime ──────────────────────────────
FROM node:20.4-alpine
# Tạo non-root user
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# Copy từ builder stage (chỉ artifact + production deps)
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]→ Image final: ~150 MB (vs ~1.5 GB nếu không tối ưu).
Câu chuyện thực tế
2022, mình refactor 1 project Node.js cho công ty fintech. Image production lúc đó 1.8 GB:
docker images
# fintech-api v1.5 1.8 GB ← khủng khiếpVấn đề:
- Pull về server mới mất 8 phút (file 1.8GB qua internet).
- CI/CD build + push mất 15 phút mỗi commit.
- Mỗi node K8s ngốn ~5GB chỉ để cache image (5 version × 1GB).
docker image history cho thấy:
SIZE CREATED BY
700MB ubuntu base + apt installs (rất nhiều)
500MB node_modules (chứa cả devDependencies)
300MB source code + assets + git history (.git committed)
200MB build tools (webpack, typescript compiler trong image)
100MB misc cacheÁp dụng 9 best practice trên 1 buổi sáng:
Base image
ubuntu→node:20-alpine. Tiết kiệm 650 MB.Multi-stage build — stage 1 với devDeps + build tools, stage 2 chỉ production deps. Tiết kiệm 500 MB.
.dockerignorethêm.git,tests/,*.log,node_modules. Build context giảm 200 MB.npm ci --only=productiontrong stage runtime. Bỏ devDependencies.RUN gộp + cleanup cùng layer. Giảm ~50 MB.
Non-root user + USER directive. Security bonus, không ảnh hưởng size.
Kết quả sau 4 tiếng:
docker images
# fintech-api v1.6 127 MB ← từ 1.8GB xuống 127MB14 lần nhỏ hơn. Impact:
- Pull về server mới: 8 phút → 40 giây.
- CI/CD build/push: 15 phút → 3 phút.
- K8s node cache: 5GB → 600MB.
- Cost transfer Docker Hub: giảm ~80%.
Bài học:
Đừng tin image size đầu tiên. Nhiều image inherit từ Dockerfile cũ, không bao giờ optimize. Audit định kỳ.
docker image historylà best friend. Cho thấy chính xác layer nào ngốn dung lượng.Multi-stage là silver bullet. Compiled language giảm 10-100 lần. Interpreted (Node, Python) giảm 5-10 lần.
.dockerignorethường bị quên — nhưng impact build time cực lớn.CI/CD nhanh = dev experience tốt. Build 3 phút thay vì 15 phút = team đổi mindset, deploy thường xuyên hơn.
Production small image = security tốt hơn. Ít component = ít attack surface. Alpine có ít CVE hơn Ubuntu.
Lab — đầy đủ
Lab 1: Build image đầu tiên
mkdir my-first-image && cd my-first-image
cat > Dockerfile <<'EOF'
FROM nginx:1.25-alpine
WORKDIR /usr/share/nginx/html
COPY index.html .
EXPOSE 80
EOF
echo "<h1>My First Docker Image!</h1>" > index.html
docker image build -t my-first:v1 .
docker container run -d -p 8080:80 --name web my-first:v1
curl http://localhost:8080
# <h1>My First Docker Image!</h1>
# Cleanup
docker container rm -f webLab 2: Quan sát layers
docker image history my-first:v1
# IMAGE CREATED CREATED BY SIZE
# abc123 1 minute ago CMD ["nginx" ...] 0B
# def456 1 minute ago EXPOSE 80 0B
# ghi789 1 minute ago COPY index.html . 52B
# ...Thấy mỗi instruction tạo layer riêng.
Lab 3: Build cache effect
# Build lần 1
time docker image build -t my-first:v1 .
# real 0m3.421s
# Build lại không đổi gì
time docker image build -t my-first:v1 .
# real 0m0.234s ← Cached!
# Đổi index.html
echo "<h1>New content!</h1>" > index.html
time docker image build -t my-first:v1 .
# real 0m0.812s ← Chỉ rebuild layer COPY trở điLab 4: Multi-stage build (Go example)
mkdir go-hello && cd go-hello
cat > main.go <<'EOF'
package main
import "fmt"
func main() { fmt.Println("Hello from Go!") }
EOF
cat > go.mod <<'EOF'
module hello
go 1.21
EOF
cat > Dockerfile <<'EOF'
# Stage 1: Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp main.go
# Stage 2: Runtime
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
EOF
docker image build -t go-hello:v1 .
docker image ls go-hello
# REPOSITORY TAG SIZE
# go-hello v1 8MB ← chỉ 8 MB!
docker container run --rm go-hello:v1
# Hello from Go!Lab 5: Push image lên Docker Hub
# 1. Login
docker login
# 2. Tag với username
docker image tag my-first:v1 yourusername/my-first:v1
# 3. Push
docker image push yourusername/my-first:v1
# 4. Verify trên hub.docker.com
# 5. Pull về từ máy khác
docker pull yourusername/my-first:v1
docker run -d -p 8080:80 yourusername/my-first:v1Lab 6: Compare size — tối ưu
Tạo Dockerfile "tệ":
# Dockerfile.bad
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get install -y python3
COPY . /app
CMD ["python3", "/app/script.py"]Tạo Dockerfile "tốt":
# Dockerfile.good
FROM python:3.11-alpine
WORKDIR /app
RUN apk add --no-cache curl
COPY script.py .
CMD ["python", "script.py"]echo "print('hello')" > script.py
docker build -f Dockerfile.bad -t app:bad .
docker build -f Dockerfile.good -t app:good .
docker images | grep app
# app bad ~250MB
# app good ~50MB→ Cùng app, image good nhỏ hơn 5 lần.
Pitfalls
docker buildkhông có.dockerignore→ toàn bộ folder upload lên daemon. Build chậm + có thể leak file nhạy cảm.Dùng
:latesttrong production → image đổi underneath, không reproducible. Pin version cụ thể.Mỗi
RUN1 dòng → nhiều layer dư, cache không hiệu quả, image to. GộpRUNbằng&&.COPY . .ngay đầu Dockerfile → mỗi code change invalidate cache. Copy deps file trước, code sau.Cài tool dev (vim, curl, gcc) trong production image → image to, attack surface lớn. Chỉ cài cái cần thiết.
Chạy với root → security risk. Tạo user thường +
USER.apt-get installkhông có--no-install-recommends→ cài hàng đống package không cần.Quên cleanup apt cache cùng layer → cache vẫn nằm trong image, ngốn ~50MB cho mỗi apt install.
Build context to (1GB+) → upload lâu, build chậm. Kiểm tra
.dockerignore.CMD shell form thay vì exec form → signal handling tệ, container không gracefully shutdown.
Hardcode secret trong Dockerfile (API key, password) → leak khi image push. Dùng
ARG+ build args, hoặc better: secret manager runtime.ADDthay vìCOPYcho file local — ADD có magic (extract, URL) không cần thiết. COPY explicit hơn.Quên
EXPOSEkhông tự-p—EXPOSEchỉ là docs. Vẫn cần-p HOST:CONTAINERkhi run.Build multi-arch fail trên Apple Silicon — image build trên Mac M1/M2 là ARM64, deploy lên server x86 fail. Dùng
docker buildxcho multi-arch.
Tóm tắt
- Image = bản đóng gói app + dependency, KHÔNG phải OS image. Không có kernel.
- Image gồm nhiều layer, mỗi layer chỉ lưu 1 lần → tiết kiệm storage + pull/push.
- Container = 1 writable layer trên top of image. Container die → writable layer mất.
- Tag = version.
:latestkhông phải "mới nhất" — chỉ là default tag. Production pin version cụ thể. - Docker Hub là registry mặc định.
docker login→ tag với username → push. - Dockerfile = text file mô tả image. Mỗi instruction tạo 1 layer.
- Build cache reuse layer không đổi → build nhanh. Cache invalidate theo thứ tự.
- 10 best practice tối ưu Dockerfile:
- Base image nhỏ (alpine)
- Multi-stage build (giảm 10-100x)
- Gộp RUN, cleanup cùng layer
.dockerignore- Non-root user
- COPY deps trước, code sau
- CMD/ENTRYPOINT exec form
- Volume cho data (không lưu data trong image)
- Pin version (hoặc digest)
- Dọn dẹp file tạm cùng layer
docker image historydebug image size.- Production image typical: < 200MB. Lớn hơn = cần refactor.
- Container chỉ HTTP trong image — SSL/HTTPS xử lý bởi reverse proxy phía trước (nginx, Traefik). Giảm complexity image + dễ scale.
Bài tiếp: 03 — Network & Storage: 4 loại network của Docker, DNS giữa containers, Volume/Bind mount/tmpfs.