Viết Dockerfile: Tự Build Docker Image Của Bạn

Ở bài trước, bạn đã hiểu Docker images và containers là gì. Bạn đã pull image từ Docker Hub và chạy thử. Nhưng làm thế nào để đóng gói app của chính mình thành một image?
Đó chính là việc của Dockerfile.
Dockerfile là gì?
Dockerfile là một text file chứa tập hợp các instruction để Docker build image. Cứ hình dung nó như một công thức nấu ăn:
- Mỗi instruction tạo ra một layer trong image
- Docker đọc từ trên xuống dưới và thực thi từng instruction theo thứ tự
- Các layer được cache lại — nếu instruction không thay đổi thì Docker dùng lại layer cũ thay vì build lại
Điểm cuối cùng là quan trọng nhất: cấu trúc Dockerfile tốt = build nhanh.
Các Instruction Cơ Bản
FROM
Mọi Dockerfile đều bắt đầu bằng FROM. Nó chỉ định base image mà image mới của bạn sẽ xây dựng trên đó.
FROM node:24
FROM node:24-alpine # Image nhẹ hơn dùng Alpine Linux
FROM ubuntu:22.04Bạn không phải bắt đầu từ con số 0 — bạn đang đứng trên vai các image có sẵn. node:24 đã có sẵn Node.js và npm. ubuntu:22.04 cho bạn một môi trường Ubuntu đầy đủ.
WORKDIR
Đặt working directory bên trong container cho tất cả các instruction phía sau (RUN, COPY, CMD, ...). Docker tự tạo thư mục nếu chưa tồn tại.
WORKDIR /appKhông có WORKDIR, các file sẽ nằm tứ tán ở thư mục root / — rất lộn xộn, khó maintain.
COPY
Copy file hoặc thư mục từ máy host vào trong image.
COPY index.js . # Copy index.js vào WORKDIR
COPY . . # Copy toàn bộ thư mục hiện tại vào WORKDIR
COPY package*.json . # Copy package.json và package-lock.jsonDấu . ở bên phải nghĩa là "vào working directory hiện tại" (tức là WORKDIR của bạn).
RUN
Thực thi một command trong quá trình build image, tạo ra một layer mới. Thường dùng để cài dependencies, compile code, hoặc bất kỳ bước setup nào.
RUN npm install express
RUN apt-get update && apt-get install -y curlMỗi instruction RUN tạo ra một layer. Nối các command bằng && để giữ số layer ở mức tối thiểu.
EXPOSE
Khai báo port mà container sẽ lắng nghe. Đây chỉ mang tính documentation — nó không tự động publish port ra ngoài máy host. Bạn vẫn cần dùng -p khi chạy docker run.
EXPOSE 3000Cứ coi nó như tài liệu cho người dùng image của bạn.
CMD
Chỉ định command mặc định chạy khi container khởi động. Mỗi Dockerfile chỉ nên có một CMD — nếu có nhiều thì chỉ cái cuối cùng có hiệu lực. Có thể bị override bằng argument truyền vào docker run.
CMD ["node", "index.js"]
CMD ["npm", "start"]Luôn dùng dạng JSON array (exec form): CMD ["executable", "arg1", "arg2"]. Dạng string chạy qua shell và có thể gây vấn đề với signal handling.
ENV
Đặt environment variable bên trong image và tất cả container tạo từ nó.
ENV NODE_ENV=production
ENV PORT=3000Các biến này có thể truy cập được cả lúc build lẫn lúc runtime.
Ví Dụ Thực Tế: Node.js App
Đây là Dockerfile hoàn chỉnh cho một app Node.js Express đơn giản.
index.js:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Docker!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});Dockerfile:
FROM node:24
WORKDIR /app
RUN npm install express
COPY index.js .
EXPOSE 3000
CMD ["node", "index.js"]Build và chạy:
# Build image (đặt tên là "my-node-app")
docker build -t my-node-app .
# Chạy container, map port 3000 từ host vào container
docker run -p 3000:3000 my-node-appTruy cập http://localhost:3000 — bạn sẽ thấy "Hello from Docker!"
Giải thích từng bước:
FROM node:24— dùng Node.js 24 làm base (đã có sẵn node và npm)WORKDIR /app— mọi lệnh tiếp theo chạy trong/appRUN npm install express— cài Express vào image lúc buildCOPY index.js .— copy source code vào/appEXPOSE 3000— khai báo app chạy ở port 3000CMD ["node", "index.js"]— khởi động app khi container start
Thứ Tự Instruction Quan Trọng: Tối Ưu Cache
Docker cache từng layer. Nếu một layer thay đổi, tất cả các layer phía sau đều bị invalidate và build lại từ đầu.
Nguyên tắc: những thứ ít thay đổi để lên trên, hay thay đổi nhất để xuống dưới.
Đây là thứ tự sai:
FROM node:24
WORKDIR /app
COPY . . # Copy tất cả trước
RUN npm install # Rồi mới install dependencies
EXPOSE 3000
CMD ["node", "index.js"]Vấn đề: mỗi lần bạn thay đổi bất kỳ source file nào — dù chỉ một dòng trong index.js — layer COPY . . thay đổi, làm mất cache của RUN npm install. npm install chạy lại từ đầu mỗi lần. Rất chậm.
Đây là thứ tự đúng:
FROM node:24
WORKDIR /app
# Copy package files trước — chỉ rebuild khi dependencies thay đổi
COPY package*.json .
RUN npm install
# Copy source code sau — thay đổi thường xuyên nhất
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]Bây giờ npm install chỉ chạy lại khi package.json hoặc package-lock.json thay đổi. Trong quá trình development hàng ngày, build rất nhanh vì chỉ layer COPY . . và các layer phía sau mới bị invalidate.
Multi-Stage Builds
Single-stage Dockerfile chứa tất cả mọi thứ: compiler, build tools, dev dependencies, source files. Image production không cần những thứ đó — nó chỉ cần output đã compile.
Multi-stage builds giải quyết điều này bằng cách dùng nhiều instruction FROM trong một Dockerfile. Mỗi FROM bắt đầu một stage mới. Image cuối cùng chỉ chứa stage cuối.
# Stage 1: Build
FROM node:24 AS builder
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build # Compile TypeScript, bundle, ...
# Stage 2: Production
FROM node:24-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist # Chỉ lấy build output
COPY --from=builder /app/package*.json .
RUN npm install --omit=dev # Chỉ cài production dependencies
EXPOSE 3000
CMD ["node", "dist/index.js"]Chi tiết quan trọng:
AS builder— đặt tên cho stage để tham chiếu về sauCOPY --from=builder— copy file từ stagebuildervào stage hiện tại- Stage
productiondùngnode:24-alpine— base image nhẹ hơn nhiều - Docker loại bỏ tất cả các stage trước khỏi image cuối cùng
Sự chênh lệch kích thước rất đáng kể:
| Approach | Base Image | Kích thước ước tính |
|---|---|---|
| Single-stage | node:24 | ~1.1 GB |
| Multi-stage | node:24-alpine | ~200 MB |
Bạn cũng có thể build đến một stage cụ thể (hữu ích khi debug):
# Build toàn bộ (mặc định lấy stage cuối)
docker build -t my-app .
# Build chỉ đến stage builder
docker build --target builder -t my-app-builder .Bảng Tóm Tắt Các Instruction
| Instruction | Mục đích |
|---|---|
FROM image:tag | Chỉ định base image |
WORKDIR /path | Đặt working directory |
COPY src dest | Copy file từ host vào image |
RUN command | Thực thi command lúc build |
EXPOSE port | Khai báo port app lắng nghe |
CMD ["cmd", "arg"] | Command mặc định khi container start |
ENV KEY=value | Đặt environment variable |
ARG NAME=default | Build-time variable (không dùng được lúc runtime) |
Tóm Lại
✅ Dockerfile là công thức để build Docker image — Docker đọc từ trên xuống dưới
✅ Mỗi instruction tạo một layer được cache — layer không đổi sẽ được tái sử dụng để build nhanh hơn
✅ Đặt COPY package*.json và RUN npm install trước COPY . . để tận dụng cache tối đa
✅ Dùng CMD ["executable", "arg"] (exec form) để xử lý signal đúng cách
✅ Multi-stage builds giảm đáng kể kích thước image bằng cách loại bỏ build tools
✅ EXPOSE chỉ là documentation — dùng -p với docker run để thực sự publish port
Series: Lộ Trình Học Docker & Kubernetes
Previous: Docker Images và Containers: Giải Thích Từ Đầu
Next: Docker Networking & Volumes
Muốn đi sâu hơn? Bài tiếp theo sẽ cover Docker networking và volumes — cách các container giao tiếp với nhau và cách persist data.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.