jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

8 MIN READ Updated JUL 11, 2026

Đâ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ỉ-đọccontainer là thực thể đang chạy. Phần này là lúc bạn ngừng chỉ kéo nginxnode 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"]
InstructionVai trò
FROMImage 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 / ADDCopy file từ build context vào image; ưu tiên COPY trừ khi cần tính năng tar/URL của ADD
RUNChạ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)
EXPOSEGhi nhận cổng app lắng nghe (không tự publish — vẫn cần -p khi docker run)
CMDLệnh mặc định khi container khởi động (có thể ghi đè)
ENTRYPOINTTiến trình chính — container xoay quanh nó (so với CMD ở dưới)

RUN vs CMD: RUN chạy khi docker build; CMD chạy khi docker 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ị .dockerignore loại).
  • -t name:tag = repository + tag. Không có tag thì Docker mặc định latest.
  • 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.

DOCKERFILE IMAGE LAYERS FROM node:24-alpine WORKDIR /app COPY package*.json . RUN npm ci COPY . . writable layer (per container) COPY . . RUN npm ci COPY package*.json . WORKDIR /app FROM node:24-alpine (base) read-only, cached & shared ↑
Each instruction adds a read-only layer (cached & shared); a container adds one thin writable layer on top

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 hi thay thế hoàn toàn.
  • ENTRYPOINT — tiến trình cố định; args thêm của docker run nố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 latestlatest khô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.3 hoặ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ùng USER (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:3000 khi docker 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.jsonserver.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:3000

2. 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 → CACHED

3. 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-docker

4. Build hai tag từ một Dockerfile: hello-docker:1.0hello-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 tags

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

Nâ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; RUN lúc build, CMD/ENTRYPOINT lúc chạy.
  • Sắp xếp cho cache: dependency trước source app.
  • .dockerignore giữ 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 latest trê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ế.