Bài 02

Image & Dockerfile: từ Docker Hub đến image của riêng mình

13 tháng 5, 2026 · 120 phút đọc

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-100MB

Image lưu ở đâu

Lưu trữ và chia sẻ image qua registry. Registry phổ biến:

  • Docker Hub — public, default. docker pull nginx lấ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 MB

Layer 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:latest

docker 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.25khô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 — :latest có 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 abc123

Tag 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ẫnn 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 Dockerdocker pull không chỉ định registry sẽ lấy từ đây.

Account và Repository

  1. Đăng ký account ở hub.docker.com (free).
  2. Tạo repository (public hoặc private).
hub.docker.com:
   yourname/myapp          → public repo
   yourname/myapp-privateprivate (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ện

Pull 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.0

Xó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 80

Bướ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 URLgiả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.gz

CMD 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 sau

Build 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 DockerfileVOLUME /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 5432

9. 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 clean

File 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ếp

Vấ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:

  1. Base image ubuntunode:20-alpine. Tiết kiệm 650 MB.

  2. Multi-stage build — stage 1 với devDeps + build tools, stage 2 chỉ production deps. Tiết kiệm 500 MB.

  3. .dockerignore thêm .git, tests/, *.log, node_modules. Build context giảm 200 MB.

  4. npm ci --only=production trong stage runtime. Bỏ devDependencies.

  5. RUN gộp + cleanup cùng layer. Giảm ~50 MB.

  6. 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 127MB

14 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:

  1. Đừng tin image size đầu tiên. Nhiều image inherit từ Dockerfile cũ, không bao giờ optimize. Audit định kỳ.

  2. docker image history là best friend. Cho thấy chính xác layer nào ngốn dung lượng.

  3. Multi-stage là silver bullet. Compiled language giảm 10-100 lần. Interpreted (Node, Python) giảm 5-10 lần.

  4. .dockerignore thường bị quên — nhưng impact build time cực lớn.

  5. 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.

  6. 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 web

Lab 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ở đi

Lab 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:v1

Lab 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

  1. docker build không có .dockerignore → toàn bộ folder upload lên daemon. Build chậm + có thể leak file nhạy cảm.

  2. Dùng :latest trong production → image đổi underneath, không reproducible. Pin version cụ thể.

  3. Mỗi RUN 1 dòng → nhiều layer dư, cache không hiệu quả, image to. Gộp RUN bằng &&.

  4. COPY . . ngay đầu Dockerfile → mỗi code change invalidate cache. Copy deps file trước, code sau.

  5. 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.

  6. Chạy với root → security risk. Tạo user thường + USER.

  7. apt-get install không có --no-install-recommends → cài hàng đống package không cần.

  8. Quên cleanup apt cache cùng layer → cache vẫn nằm trong image, ngốn ~50MB cho mỗi apt install.

  9. Build context to (1GB+) → upload lâu, build chậm. Kiểm tra .dockerignore.

  10. CMD shell form thay vì exec form → signal handling tệ, container không gracefully shutdown.

  11. Hardcode secret trong Dockerfile (API key, password) → leak khi image push. Dùng ARG + build args, hoặc better: secret manager runtime.

  12. ADD thay vì COPY cho file local — ADD có magic (extract, URL) không cần thiết. COPY explicit hơn.

  13. Quên EXPOSE không tự -pEXPOSE chỉ là docs. Vẫn cần -p HOST:CONTAINER khi run.

  14. 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 buildx cho 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. :latest khô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:
    1. Base image nhỏ (alpine)
    2. Multi-stage build (giảm 10-100x)
    3. Gộp RUN, cleanup cùng layer
    4. .dockerignore
    5. Non-root user
    6. COPY deps trước, code sau
    7. CMD/ENTRYPOINT exec form
    8. Volume cho data (không lưu data trong image)
    9. Pin version (hoặc digest)
    10. Dọn dẹp file tạm cùng layer
  • docker image history debug 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.

← Bài trước
Bài 01
Bài tiếp theo →
Bài 03
Zalo tư vấn