Nếu bạn đã tìm hiểu qua các thành phần cơ bản của Docker, thì chắc hẳn bạn cũng đã biết trái tim của mọi Docker image chính là Dockerfile - một "bản thiết kế" hay "công thức nấu ăn" chỉ dẫn Docker cách xây dựng nên image cho ứng dụng của bạn.
Việc viết ra một Dockerfile trông có vẻ đơn giản, nhưng để viết một Dockerfile tốt, hiệu quả, bảo mật, và tối ưu lại là cả một nghệ thuật. Bài viết này sẽ giúp bạn chinh phục nghệ thuật đó, dù bạn là người mới bắt đầu hay đã có kinh nghiệm.
Dockerfile là gì? Tại sao nó lại quan trọng?
Hãy tưởng tượng bạn đang chuẩn bị một món ăn phức tạp. Thay vì mỗi lần nấu lại phải nhớ từng bước, bạn viết ra một công thức chi tiết: nguyên liệu cần gì, sơ chế ra sao, nấu nướng thế nào.
Dockerfile chính là công thức đó cho ứng dụng của bạn.
Nó là một tệp văn bản đơn giản, không có phần mở rộng, chứa một loạt các chỉ thị (instructions) theo thứ tự. Docker sẽ đọc tệp này và thực thi từng chỉ thị để tự động tạo ra một Docker image.
Tại sao phải dùng Dockerfile?
- Tự động hóa (Automation): Chỉ cần viết một lần, bạn có thể build lại image y hệt trên bất kỳ máy nào có Docker.
- Nhất quán (Consistency): Đảm bảo môi trường chạy ứng dụng của bạn (trên máy local, staging, hay production) là hoàn toàn giống nhau, xóa bỏ triệt để câu nói "Ơ, trên máy em chạy được mà!".
- Kiểm soát phiên bản (Versioning): Bạn có thể đưa Dockerfile vào hệ thống quản lý phiên bản (như Git) để theo dõi mọi thay đổi trong môi trường của ứng dụng.
- Tính di động (Portability): Image được tạo ra có thể chạy ở bất cứ đâu, từ laptop của developer đến các dịch vụ đám mây.
"Giải phẫu" một Dockerfile: Các chỉ thị cốt lõi
Một Dockerfile được tạo nên từ các chỉ thị. Mỗi chỉ thị là một "bước" trong công thức. Dưới đây là những chỉ thị quan trọng và phổ biến nhất mà bạn cần nắm vững.
FROM - Nền tảng của mọi thứ
Mọi Dockerfile đều phải bắt đầu bằng chỉ thị FROM
. Nó xác định image cơ sở (base image) mà bạn sẽ build lên trên. Việc chọn base image đúng đắn là cực kỳ quan trọng để tối ưu hóa kích thước và bảo mật.
node:18-alpine
: Một image Node.js gọn nhẹ dựa trên Alpine Linux.python:3.10-slim
: Một image Python được tối giản.ubuntu:22.04
: Một image Ubuntu đầy đủ tính năng.
Ví dụ:
# Sử dụng phiên bản 18 của Node.js với hệ điều hành Alpine Linux gọn nhẹ
FROM node:18-alpine
WORKDIR - Thiết lập không gian làm việc
Chỉ thị này thiết lập thư mục làm việc cho tất cả các chỉ thị theo sau nó (RUN
, CMD
, COPY
, ADD
). Nếu thư mục không tồn tại, nó sẽ được tạo ra. Đây là cách làm sạch sẽ và an toàn hơn nhiều so với việc dùng RUN cd /my-app
.
Ví dụ:
# Thiết lập thư mục làm việc bên trong container là /app
WORKDIR /app
COPY và ADD - Đưa "nguyên liệu" vào Image
Cả hai chỉ thị này đều dùng để sao chép file từ máy host của bạn vào bên trong image. Tuy nhiên, hãy luôn ưu tiên sử dụng COPY
trừ khi bạn thực sự cần tính năng của ADD
.
COPY
: Đơn giản là sao chép file và thư mục. Rõ ràng, dễ đoán.ADD
: Có thêm một vài "phép thuật" như tự động giải nén file tar và có thể lấy file từ URL (tính năng này không được khuyến khích vì làm tăng kích thước image và kém an toàn).
Ví dụ:
# Sao chép file package.json và package-lock.json vào thư mục làm việc hiện tại (/app)
COPY package*.json ./
# Sao chép toàn bộ mã nguồn từ thư mục hiện tại của host vào thư mục làm việc của container
COPY . .
RUN - Thực thi mệnh lệnh
Đây là chỉ thị dùng để thực thi bất kỳ lệnh nào bên trong image, ví dụ như cài đặt các gói phụ thuộc, tạo thư mục, hay biên dịch mã nguồn. Mỗi lệnh RUN
sẽ tạo ra một "lớp" (layer) mới cho image.
Ví dụ:
# Chạy lệnh npm install để cài đặt các gói phụ thuộc từ package.json
RUN npm install
# Ví dụ trên Ubuntu: Cập nhật danh sách gói và cài đặt git
RUN apt-get update && apt-get install -y git
EXPOSE - Mở cổng giao tiếp
Chỉ thị này thông báo cho Docker rằng container sẽ lắng nghe trên một cổng mạng cụ thể nào đó khi chạy. Lưu ý: EXPOSE
không thực sự mở cổng đó ra ngoài. Nó chỉ đóng vai trò như một tài liệu hướng dẫn. Để public cổng, bạn cần dùng cờ -p
hoặc -P
khi chạy docker run
.
Ví dụ:
# Thông báo rằng ứng dụng bên trong sẽ chạy trên cổng 3000
EXPOSE 3000
CMD và ENTRYPOINT - Khởi động ứng dụng
Đây là hai chỉ thị thường gây nhầm lẫn nhất. Cả hai đều xác định lệnh sẽ được thực thi khi container khởi chạy.
CMD
: Cung cấp một lệnh mặc định. Lệnh này có thể dễ dàng bị ghi đè khi bạn chạy container. Một Dockerfile chỉ nên có mộtCMD
.- Mục đích: Cung cấp lệnh mặc định cho một container có thể thực thi.
ENTRYPOINT
: Cấu hình container để nó hoạt động như một file thực thi chính. Các tham số bạn truyền vào khidocker run
sẽ được nối vào sauENTRYPOINT
.- Mục đích: Tạo ra một image chuyên dụng cho một tác vụ cụ thể.
Cách kết hợp phổ biến: Dùng ENTRYPOINT
để xác định file thực thi chính và CMD
để cung cấp các tham số mặc định cho file thực thi đó.
Ví dụ:
# Cách 1: Sử dụng CMD (phổ biến cho các ứng dụng web)
# Lệnh mặc định khi container chạy. Có thể bị ghi đè.
# Ví dụ: docker run my-app-image sh (sẽ chạy shell thay vì node)
CMD ["node", "server.js"]
# Cách 2: Sử dụng ENTRYPOINT và CMD kết hợp
# Container sẽ luôn chạy "npm".
ENTRYPOINT ["npm"]
# Tham số mặc định là "start".
# Chạy `docker run my-app-image` tương đương `npm start`
# Chạy `docker run my-app-image test` tương đương `npm test`
CMD ["start"]
Ví dụ thực tế: Dockerize một ứng dụng Node.js
Hãy cùng nhau áp dụng những kiến thức trên để "đóng gói" một ứng dụng Node.js Express đơn giản.
Cấu trúc thư mục:
/my-node-app
|-- /src
| |-- index.js
|-- package.json
|-- Dockerfile
Nội dung Dockerfile
:
# Giai đoạn 1: Chọn base image
FROM node:18-alpine AS base
# Giai đoạn 2: Thiết lập môi trường
WORKDIR /app
# Giai đoạn 3: Sao chép file package và cài đặt dependencies
# Tận dụng caching: chỉ chạy lại npm install khi package.json thay đổi
COPY package*.json ./
RUN npm install --only=production
# Giai đoạn 4: Sao chép mã nguồn
COPY . .
# Giai đoạn 5: Mở cổng và xác định lệnh khởi chạy
EXPOSE 3000
CMD ["node", "src/index.js"]
Build và chạy image:
# 1. Build image với tên là 'my-node-app'
docker build -t my-node-app .
# 2. Chạy container từ image vừa tạo
# -p 8080:3000: Ánh xạ cổng 8080 của máy host vào cổng 3000 của container
# -d: Chạy container ở chế độ nền (detached)
docker run -d -p 8080:3000 my-node-app
Bây giờ, ứng dụng Node.js của bạn đã chạy trong một container và có thể truy cập qua http://localhost:8080
.
Tuyệt chiêu tối ưu Dockerfile 💡
Viết Dockerfile chạy được là một chuyện, viết Dockerfile tối ưu lại là chuyện khác. Dưới đây là các bí kíp giúp image của bạn nhỏ hơn, build nhanh hơn và an toàn hơn.
Sử dụng .dockerignore
Tương tự .gitignore
, file .dockerignore
cho phép bạn loại bỏ những file và thư mục không cần thiết (như node_modules
, .git
, file log,...) khỏi quá trình build. Điều này giúp giảm kích thước image và tăng tốc độ build.
Ví dụ .dockerignore
:
.git
node_modules
npm-debug.log
Dockerfile
.dockerignore
Tận dụng Layer Caching
Docker build image theo từng lớp (layer), tương ứng với mỗi chỉ thị trong Dockerfile. Nếu một lớp không thay đổi, Docker sẽ tái sử dụng nó từ cache thay vì build lại.
Mẹo: Sắp xếp các chỉ thị từ ít thay đổi nhất đến thay đổi thường xuyên nhất.
- Sai ❌:
(Mỗi lần bạn thay đổi một file mã nguồn,COPY . . RUN npm install
COPY
sẽ chạy lại vànpm install
cũng phải chạy lại theo) - Đúng ✅:
(COPY package*.json ./ RUN npm install COPY . .
npm install
chỉ chạy lại khipackage.json
thay đổi, còn việc thay đổi code sẽ không ảnh hưởng.)
Giảm kích thước Image với Multi-stage Builds
Đây là kỹ thuật cực kỳ mạnh mẽ. Ý tưởng là bạn dùng một image "to béo" chứa đầy đủ công cụ để biên dịch, build (gọi là stage builder
), sau đó chỉ sao chép kết quả cuối cùng (file thực thi, tài sản tĩnh) sang một image "gọn nhẹ" để chạy production (stage final
).
Ví dụ cho ứng dụng React:
# --- Giai đoạn BUILD ---
# Dùng image node đầy đủ để build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Tạo bản build production
RUN npm run build
# --- Giai đoạn FINAL ---
# Dùng một web server siêu nhẹ để phục vụ file tĩnh
FROM nginx:1.23-alpine
# Sao chép thư mục build từ giai đoạn 'builder' vào thư mục của nginx
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Kết quả? Image cuối cùng chỉ chứa Nginx và các file tĩnh đã được build, hoàn toàn không có Node.js hay node_modules
, giúp giảm kích thước từ hàng trăm MB xuống chỉ còn vài chục MB.
Bảo mật: Chạy với người dùng không phải root 🛡️
Mặc định, các container chạy với quyền root
, tiềm ẩn rủi ro bảo mật. Hãy tạo một người dùng riêng và chuyển sang người dùng đó.
...
# Tạo user và group riêng
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Chuyển sang user mới
USER appuser
...
CMD ["node", "src/index.js"]
Kết luận: Dockerfile không chỉ là một tệp cấu hình
Dockerfile không chỉ là một tệp cấu hình, nó là bản tuyên ngôn về cách ứng dụng của bạn được xây dựng và vận hành. Bằng cách nắm vững các chỉ thị cốt lõi, áp dụng các kỹ thuật tối ưu và luôn tư duy về hiệu suất cũng như bảo mật, bạn có thể tạo ra những Docker image hoàn hảo, làm nền tảng vững chắc cho bất kỳ hệ thống nào.
Chúc bạn thành công trên hành trình chinh phục Docker!