Docker for Developers · Part 2 — Images & the Dockerfile
Tự build image với Dockerfile: instruction, layer/cache, multi-stage, .dockerignore, CMD/ENTRYPOINT và chiến lược tag.
Đây là Phần 2 của chặng nền tảng về Docker → Compose → Kubernetes. Ở Phần 1 — Container, Image & mô hình tư duy bạn đã học image là artifact chỉ-đọc và container là thực thể đang chạy. Phần này là lúc bạn ngừng chỉ kéo nginx và node từ Docker Hub và tự build image bằng Dockerfile.
Một Dockerfile tốt không chỉ “build được”. Nó phải tái lập được, cache tốt, không rò secret, chạy đúng tiến trình chính, và tạo ra image đủ nhỏ để deploy nhiều lần mỗi ngày mà không đau. Mỗi phần kết thúc bằng bài tập; hãy làm trên máy thật.
Giải phẫu Dockerfile — các chỉ thị lõi
Một Dockerfile là hợp đồng build dạng văn bản: từ source code và build context, nó tạo ra một image có thể chạy ở nơi khác. Mỗi dòng là một chỉ thị; nhiều chỉ thị tạo thành layer image khi bạn build. Một app tối giản có thể như sau:
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
| Instruction | Vai trò |
|---|---|
FROM | Image nền — mọi Dockerfile bắt đầu từ đây |
WORKDIR | Đặt thư mục làm việc trong image (tạo thư mục nếu chưa có) |
COPY / ADD | Copy file từ build context vào image; ưu tiên COPY trừ khi cần tính năng tar/URL của ADD |
RUN | Chạy lệnh lúc build (cài package, biên dịch) |
ENV | Đặt biến môi trường (dùng khi build và khi chạy) |
EXPOSE | Ghi nhận cổng app lắng nghe (không tự publish — vẫn cần -p khi docker run) |
CMD | Lệnh mặc định khi container khởi động (có thể ghi đè) |
ENTRYPOINT | Tiến trình chính — container xoay quanh nó (so với CMD ở dưới) |
RUNvsCMD:RUNchạy khidocker build;CMDchạy khidocker run. Nhầm hai cái là lỗi kinh điển của người mới. Ở cấp senior, câu hỏi cần hỏi thêm là: “lệnh nào tạo artifact, lệnh nào là process production?”
Build & gắn tag image
Từ thư mục chứa Dockerfile (và file app), build và đặt tên kết quả:
docker build -t myapp:1.0 .
# │ │ └── build context (usually ".")
# │ └────── tag (version or label)
# └───────────── repository name
- Build context = thư mục Docker gửi lên daemon (mọi thứ không bị
.dockerignoreloại). -t name:tag= repository + tag. Không có tag thì Docker mặc địnhlatest.- Tag là nhãn có thể đổi; image ID/digest mới là định danh nội dung. Dev có thể dùng tag tiện lợi, production nên có version rõ hoặc digest.
Liệt kê và chạy thứ vừa build:
docker images myapp
docker run --rm -p 3000:3000 myapp:1.0
Cùng một image ID có thể gắn nhiều tên:
docker tag myapp:1.0 myapp:1.0-prod
docker tag myapp:1.0 registry.example.com/myapp:1.0
Xem metadata (layer, env, cmd) mà không cần chạy:
docker image inspect myapp:1.0
docker history myapp:1.0 # which Dockerfile steps created which layers
Layer & build cache
Mỗi chỉ thị Dockerfile thay đổi filesystem tạo một layer chỉ-đọc mới. Build lại nhanh vì Docker tái dùng layer cache khi chỉ thị và input không đổi.
Khi build lại, Docker tái dùng mọi layer cache cho tới chỗ thay đổi đầu tiên, rồi rebuild phần còn lại:
docker build (first time) docker build (code changed only)
┌─────────────────────────┐ ┌─────────────────────────┐
│ FROM node:24-alpine │ CACHE ✓ │ FROM node:24-alpine │ CACHE ✓
├─────────────────────────┤ ├─────────────────────────┤
│ COPY package*.json │ CACHE ✓ │ COPY package*.json │ CACHE ✓
├─────────────────────────┤ ├─────────────────────────┤
│ RUN npm ci │ CACHE ✓ │ RUN npm ci │ CACHE ✓
├─────────────────────────┤ ├─────────────────────────┤
│ COPY . . │ NEW │ COPY . . │ REBUILD ──► invalidates below
├─────────────────────────┤ ├─────────────────────────┤
│ CMD ["node","server.js"]│ NEW │ CMD ... │ REBUILD
└─────────────────────────┘ └─────────────────────────┘
Quy tắc vô hiệu cache: một layer rebuild thì mọi layer phía sau cũng rebuild. Đó là lý do thứ tự quan trọng.
Mẹo copy dependency trước
Tệ — copy hết trước; đổi code làm hỏng cache npm ci:
COPY . .
RUN npm ci --omit=dev
Tốt — copy lockfile, cài, rồi mới copy source:
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
Cùng ý với Python (requirements.txt rồi pip install, rồi code) hoặc Go (go.mod / go.sum rồi go mod download).
Build sạch khi debug cache lạ:
docker build --no-cache -t myapp:1.0 .
Tệp .dockerignore
Build context là mọi thứ trong . (trừ ignore) — kể cả node_modules, .git và secret local nếu bạn không cẩn thận. .dockerignore (cú pháp giống .gitignore) giữ rác ra ngoài:
node_modules
.git
.env
.env.*
dist
coverage
*.md
Dockerfile*
.dockerignore
Context nhỏ hơn, ít lộ secret, không ghi đè node_modules trong image bằng bản trên host.
Build nhiều giai đoạn
Dockerfile multi-stage có nhiều hơn một FROM — thường là giai đoạn builder có compiler và devDependencies, và giai đoạn runtime chỉ copy artifact cần thiết.
STAGE "builder" STAGE "runtime" (final image)
┌──────────────────────┐ ┌──────────────────────┐
│ node:24-alpine │ COPY │ node:24-alpine │
│ npm ci + npm run │ ────────► │ production deps only │
│ build → dist/ │ artifact │ + dist/ │
│ (~400 MB tools) │ │ (~80 MB typical) │
└──────────────────────┘ └──────────────────────┘
discarded what you ship
Ví dụ — build ở builder, chạy gọn ở runtime:
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine AS runtime
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
COPY --from=builder chỉ kéo artifact vào image cuối — so với docker images (thường nhỏ hơn hàng trăm MB so với single-stage có dev tools).
CMD vs ENTRYPOINT
CMD— lệnh mặc định khi container khởi động;docker run myapp echo hithay thế hoàn toàn.ENTRYPOINT— tiến trình cố định; args thêm củadocker runnối vào (trừ--entrypoint).
Ưu tiên exec form CMD ["node", "server.js"] thay vì shell form — PID 1 nhận signal đúng. Kết hợp ENTRYPOINT ["node"] + CMD ["server.js"] khi cần binary cố định với args mặc định đổi được.
Các bẫy thường gặp
- Bẫy tag
latest—latestkhông phải “bản ổn định mới nhất”; chỉ là nhãn có thể đổi. Ghim version trên prod:myapp:1.0.3hoặc digest. - Copy hết trước — phá cache mỗi lần sửa code; copy lockfile/dependency trước.
- Image khổng lồ — dùng base full thay vì slim/Alpine, nhét devDependencies, không multi-stage.
- Chạy root — user mặc định là
root. DùngUSER(non-root) — đào sâu ở Phần 7. - Không có
.dockerignore— build chậm, context phình, secret lọt vào layer. EXPOSE≠ publish cổng — vẫn cần-p 3000:3000khidocker run.
Bảng tra nhanh
# build & tag
docker build -t myapp:1.0 .
docker build --no-cache -t myapp:1.0 .
docker tag myapp:1.0 myapp:latest
docker images
docker rmi myapp:1.0
# inspect
docker image inspect myapp:1.0
docker history myapp:1.0
# run what you built
docker run --rm -p 3000:3000 myapp:1.0
docker run --rm myapp:1.0 node -v # override CMD
# cleanup build cache
docker builder prune
Bài tập / Exercises
Tạo thư mục docker-lab-02/ và làm bài trong đó. Mỗi bài độc lập.
1. Viết Dockerfile cho app Node nhỏ và build thành hello-docker:1.0. Tạo package.json và server.js như mô tả.
Lời giải
FROM node:24-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js .
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]docker build -t hello-docker:1.0 .
docker run --rm -p 3000:3000 hello-docker:1.0
curl http://localhost:30002. Sắp xếp lại: copy package.json + npm install trước server.js; sửa chỉ server.js, build lại — RUN npm install phải CACHED.
Lời giải
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --omit=dev
COPY server.js .
CMD ["node", "server.js"]# first build
docker build -t hello-docker:1.0 .
# edit server.js (e.g. change the response string)
docker build -t hello-docker:1.0 .
# watch output: RUN npm install → CACHED3. Chuyển bài 1 sang multi-stage: stage builder có bước build (vd echo "built" > dist.txt), stage runtime chỉ copy server.js + dist.txt.
Lời giải
FROM node:24-alpine AS builder
WORKDIR /app
COPY server.js .
RUN mkdir dist && printf 'build-v1\n' > dist/build-stamp.txt
FROM node:24-alpine AS runtime
WORKDIR /app
COPY server.js .
COPY --from=builder /app/dist/build-stamp.txt ./dist/
CMD ["node", "server.js"]docker build -t hello-docker:ms .
docker run --rm hello-docker:ms cat dist/build-stamp.txt
docker images hello-docker4. Build hai tag từ một Dockerfile: hello-docker:1.0 và hello-docker:1.0-local, liệt kê và xác nhận cùng IMAGE ID.
Lời giải
docker build -t hello-docker:1.0 .
docker tag hello-docker:1.0 hello-docker:1.0-local
docker images hello-docker
# same IMAGE ID column for both tags5. Chạy image ở chế độ detached cổng 3100, kiểm tra bằng curl, xem log, rồi dừng và xóa.
Lời giải
docker run --name hello -d -p 3100:3000 hello-docker:1.0
curl http://localhost:3100
docker logs hello
docker stop hello && docker rm helloNâng cao: Thêm .dockerignore loại thư mục dummy lớn, so sánh kích thước context khi build (dòng “transferring context”).
Điểm chính
- Dockerfile là công thức từng layer;
RUNlúc build,CMD/ENTRYPOINTlúc chạy. - Sắp xếp cho cache: dependency trước source app.
.dockerignoregiữ context nhỏ và chặn secret.- Multi-stage đưa image runtime gọn, không kéo compiler và dev tool.
- Tag có chủ đích — đừng phụ thuộc
latesttrên production.
Tiếp theo
Phần 3 — Volume, bind mount và biến môi trường — để dữ liệu sống sót khi container được thay thế.