[Docker Basics] Cách viết Dockerfile chuẩn & tối ưu cho người mới

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.

Cách viết Dockerfile chuẩn & tối ưu cho người mới

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.

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ột CMD.
    • 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 khi docker run sẽ được nối vào sau ENTRYPOINT.
    • 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 ❌:
    COPY . .
    RUN npm install
    
    (Mỗi lần bạn thay đổi một file mã nguồn, 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 khi package.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!

Bài viết liên quan

[Docker Basics] Hướng dẫn cách build Docker Image đơn giản, hiệu quả

Bạn muốn build Docker Image một cách chuyên nghiệp? Bài viết này sẽ hướng dẫn chi tiết từng bước, từ Dockerfile cơ bản đến các mẹo tối ưu hóa để Docker Image của bạn gọn nhẹ và hiệu quả.

[Docker Basics] Hướng dẫn cài đặt Docker chi tiết trên Windows, macOS và Linux

Làm thế nào để cài đặt Docker? Hướng dẫn từng bước cho hệ điều hành Windows, Mac và Linux. Giúp bạn cài Docker Desktop và Docker Engine một cách dễ dàng nhất.

[Docker Basics] Docker Compose là gì? Tầm quan trọng và cách sử dụng hiệu quả

Tìm hiểu Docker Compose là gì và tại sao công cụ này lại quan trọng khi làm việc với Docker. Khám phá cách triển khai các ứng dụng multi-container dễ dàng hơn bao giờ hết.

[Docker Basics] Docker Hub: Cách lưu trữ và quản lý Docker Image chuyên nghiệp

Bạn đang gặp khó khăn trong việc quản lý Docker Image? Tìm hiểu Docker Hub và các bí kíp để đẩy, kéo và quản lý kho Image của bạn một cách chuyên nghiệp.