Back to blog

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

dockerdockerfiledevopscontainerstutorial
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.04

Bạ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 /app

Khô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.json

Dấ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 curl

Mỗ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 3000

Cứ 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=3000

Cá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-app

Truy cập http://localhost:3000 — bạn sẽ thấy "Hello from Docker!"

Giải thích từng bước:

  1. FROM node:24 — dùng Node.js 24 làm base (đã có sẵn node và npm)
  2. WORKDIR /app — mọi lệnh tiếp theo chạy trong /app
  3. RUN npm install express — cài Express vào image lúc build
  4. COPY index.js . — copy source code vào /app
  5. EXPOSE 3000 — khai báo app chạy ở port 3000
  6. CMD ["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ề sau
  • COPY --from=builder — copy file từ stage builder vào stage hiện tại
  • Stage production dùng node: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ể:

ApproachBase ImageKích thước ước tính
Single-stagenode:24~1.1 GB
Multi-stagenode: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

InstructionMục đích
FROM image:tagChỉ định base image
WORKDIR /pathĐặt working directory
COPY src destCopy file từ host vào image
RUN commandThực thi command lúc build
EXPOSE portKhai 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=defaultBuild-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.