Viết Dockerfile và Build Cache

K
Kai··5 min read

Tới giờ ta toàn chạy image người khác làm. Bài này tự dựng image cho ứng dụng của mình bằng Dockerfile — một file văn bản mô tả cách dựng image. Ta cũng mổ xẻ build cache, thứ quyết định build của bạn nhanh hay chậm.

Dockerfile là gì

Dockerfile là công thức dựng image: mỗi dòng là một chỉ thị, Docker thực hiện lần lượt từ trên xuống, và (nhớ Bài 2) phần lớn chỉ thị tạo ra một layer mới trong image.

Ta đóng gói một app Node.js nhỏ. Tạo thư mục dự án với ba file.

package.json:

{
  "name": "demo",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": { "start": "node server.js" },
  "dependencies": { "express": "^4.19.2" }
}

server.js:

const express = require("express");
const app = express();
app.get("/", (req, res) => res.send("Xin chao tu container"));
app.listen(3000, () => console.log("Server chay tren cong 3000"));

Dockerfile:

# Layer nền: image Node chính thức, bản alpine cho nhẹ
FROM node:20-alpine

# Thư mục làm việc bên trong image
WORKDIR /app

# Copy riêng file khai báo dependency TRƯỚC
COPY package.json ./

# Cài dependency
RUN npm install --omit=dev

# Copy phần code còn lại SAU
COPY . .

# Cổng ứng dụng lắng nghe (mang tính tài liệu)
EXPOSE 3000

# Lệnh chạy khi container khởi động
CMD ["npm", "start"]

Các chỉ thị Dockerfile cốt lõi

  • FROM — image nền để xây tiếp. Mọi Dockerfile bắt đầu bằng FROM.
  • WORKDIR — đặt thư mục làm việc; các lệnh sau chạy trong thư mục này.
  • COPY — copy file từ máy build vào image. (Có ADD mạnh hơn nhưng dễ gây bất ngờ; nên dùng COPY.)
  • RUN — chạy một lệnh lúc build (cài gói, biên dịch...). Mỗi RUN tạo một layer.
  • ENV — đặt biến môi trường.
  • EXPOSE — khai báo cổng ứng dụng dùng. Chỉ là tài liệu, không tự mở cổng (vẫn cần -p khi run).
  • CMD — lệnh chạy mặc định khi container khởi động (chạy lúc run, không phải lúc build).

Phân biệt RUNCMD: RUN chạy lúc build image (kết quả nướng vào layer); CMD chạy lúc khởi động container. Một nhầm lẫn phổ biến của người mới là tưởng CMD chạy lúc build.

Phân biệt CMDENTRYPOINT: cả hai định nghĩa lệnh chạy khi container start. Khác biệt: tham số bạn truyền vào docker run <image> <args> sẽ thay thế CMD, nhưng được nối thêm vào ENTRYPOINT. Mẫu hay dùng: ENTRYPOINT là chương trình cố định, CMD là tham số mặc định có thể ghi đè. Lúc mới học, dùng CMD là đủ.

Build image

Từ thư mục chứa Dockerfile:

docker build -t demo:v1 .

-t demo:v1 đặt tên:tag cho image. Dấu . cuối là build context — thư mục Docker gửi cho daemon để build (Bài 1: build do daemon làm, nên nó cần được gửi các file).

Chạy thử image vừa dựng:

docker run --rm -p 3000:3000 demo:v1

Mở http://localhost:3000 thấy "Xin chao tu container". Ctrl+C để dừng.

Build cache: vì sao lần build sau nhanh hơn

Docker cache từng layer. Khi build lại, với mỗi chỉ thị nó kiểm tra: layer này đã có trong cache và đầu vào không đổi chưa? Nếu rồi thì dùng lại (CACHED), không chạy lại.

Build lần hai mà không sửa gì:

docker build -t demo:v1 .
 => CACHED [2/5] WORKDIR /app
 => CACHED [3/5] COPY package.json ./
 => CACHED [4/5] RUN npm install --omit=dev
 => CACHED [5/5] COPY . .

Mọi layer đều CACHED — build gần như tức thì, không cài lại dependency.

Quy tắc quan trọng nhất: cache đổ theo chuỗi

Theo tài liệu Docker: khi một layer thay đổi, "layer đó cần build lại", và "mọi layer phía sau nó cũng phải chạy lại". Cache chỉ giữ được tới layer đầu tiên bị thay đổi; từ đó trở xuống build lại hết.

Đây là lý do thứ tự lệnh trong Dockerfile quan trọng. Thử sửa server.js (đổi nội dung trả về) rồi build lại:

 => CACHED [2/5] WORKDIR /app
 => CACHED [3/5] COPY package.json ./
 => CACHED [4/5] RUN npm install --omit=dev      ← vẫn CACHED!
 =>        [5/5] COPY . .                          ← build lại từ đây

RUN npm install vẫn được cache dù ta đã sửa code, vì nó nằm trước COPY . . và đầu vào của nó (package.json) không đổi. Chỉ COPY . . trở xuống build lại.

   Dockerfile              build sau khi đổi server.js
   ─────────────────────────────────────────────────
   FROM node:20-alpine     ✓ CACHED
   WORKDIR /app            ✓ CACHED
   COPY package.json ./    ✓ CACHED   (package.json không đổi)
   RUN npm install         ✓ CACHED   ← KHÔNG cài lại, tiết kiệm nhiều
   COPY . .                ✗ rebuild  ← server.js đổi, từ đây build lại
   CMD ["npm","start"]     ✗ rebuild

Vì sao tách COPY package.json rồi mới COPY . .

Nếu bạn gộp thành một COPY . . ngay trước RUN npm install, thì mỗi lần sửa bất kỳ file code nào cũng làm COPY thay đổi, kéo theo npm install build lại — cài lại toàn bộ dependency, rất chậm.

Bằng cách copy riêng package.json trước rồi cài, layer npm install chỉ build lại khi dependency đổi (tức package.json đổi), không phải khi code đổi. Quy tắc tổng quát từ tài liệu: đặt lệnh ít thay đổi (cài đặt) trước, lệnh hay thay đổi (copy code) sau.

.dockerignore: đừng gửi rác vào build context

Khi build, Docker gửi toàn bộ build context (thư mục .) cho daemon. Thêm file .dockerignore để loại những thứ không cần, giúp build nhanh và image gọn:

node_modules
npm-debug.log
.git
.env

Loại node_modules đặc biệt quan trọng với Node: ta muốn dependency được cài bên trong image bằng npm install, không phải copy node_modules của máy host (có thể sai kiến trúc, vd cài trên macOS arm64 nhưng image chạy linux).

🧹 Dọn dẹp

docker rmi demo:v1
docker builder prune     # xóa build cache cũ để lấy lại đĩa

docker builder prune dọn riêng phần cache của quá trình build (khác với image). Xem cache build đang chiếm bao nhiêu: docker system df.

Tổng kết

Dockerfile mô tả cách dựng image; mỗi RUN/COPY tạo một layer. Build cache dùng lại layer không đổi, nhưng một layer đổi thì mọi layer sau nó build lại — nên sắp xếp từ ít-thay-đổi (cài dependency) tới hay-thay-đổi (copy code), và copy package.json trước để giữ layer cài đặt được cache. .dockerignore giữ build context gọn.

Image của bạn giờ chạy được, nhưng mọi dữ liệu nó ghi ra sẽ mất khi container bị xóa (nhớ writable layer ở Bài 2). Bài 6 giải quyết việc đó: volume và bind mount để lưu dữ liệu bền vững.