Docker từ A-Z (Phần 3): Docker hóa Backend Node.js đầu tiên

Lý thuyết đủ rồi, giờ là lúc chúng ta xắn tay áo lên và thực hành thôi anh em! Đây mới là phần hấp dẫn nhất nè. Ở phần 2, chúng ta đã tìm hiểu về các mảnh ghép như Image, Container, Dockerfile rồi đúng không?
(Bạn nào chưa đọc hoặc muốn ôn lại các khái niệm đó thì có thể xem lại tại đây nhé: Docker từ A-Z (Phần 2): Khám phá các mảnh ghép cốt lõi)
Trong phần 3 này, chúng ta sẽ cùng nhau làm một việc cực kỳ thiết thực: “Docker hóa” một ứng dụng Node.js (dùng Express) đơn giản. Tức là chúng ta sẽ viết một file Dockerfile
để đóng gói ứng dụng này thành một Docker Image, sau đó build image đó và chạy nó dưới dạng một Docker Container độc lập.
Nghe phức tạp hả? Không hề đâu, hãy cùng mình bắt đầu trên chuyến tàu đầu tiên nhé. Let’s go!
Chuẩn bị đồ nghề
Trước khi bắt đầu, anh em cần chuẩn bị vài thứ sau:
- Docker Desktop: Đảm bảo bạn đã cài đặt Docker Desktop (cho Windows/Mac) hoặc Docker Engine (cho Linux). Nếu chưa, bạn có thể xem hướng dẫn cài đặt trên trang chủ của Docker. Đây là công cụ bắt buộc phải có.
- Trình soạn thảo Code: Bất kỳ editor nào bạn quen thuộc (VS Code, Sublime Text, Atom…).
- Node.js và npm/yarn: Lưu ý quan trọng: Bạn chỉ cần cài Node.js trên máy để tạo project ban đầu và viết code thôi. Sau khi “Docker hóa” xong, bạn không cần cài Node.js trên máy chủ hoặc máy của người khác để chạy ứng dụng nữa, vì Node.js đã được đóng gói bên trong Docker Image rồi! Đây chính là một trong những cái hay của Docker đó.
- Một ứng dụng Node.js đơn giản: Chúng ta sẽ tự tạo ngay bây giờ.
Tạo ứng dụng Node.js “Hello Docker”
Chúng ta sẽ tạo một web server siêu đơn giản bằng Express. Mở terminal hoặc command prompt của bạn lên:
- Tạo một thư mục mới cho dự án, ví dụ:
mkdir my-node-app && cd my-node-app
- Khởi tạo dự án Node.js:
npm init -y
(Hoặcyarn init -y
) - Cài đặt Express:
npm install express
(Hoặcyarn add express
) - Tạo file
server.js
(hoặcindex.js
) với nội dung sau:
// server.js
const express = require('express');
const app = express();
const PORT = 3000; // Chúng ta sẽ để app chạy ở port 3000 bên trong container
app.get('/', (req, res) => {
res.send('Hello Docker! Chào mừng bạn đến với container đầu tiên.');
});
app.get('/api/users', (req, res) => {
// Trả về một JSON mẫu
res.json([
{ id: 1, name: 'Siucode User 1' },
{ id: 2, name: 'Docker Fan' }
]);
});
app.listen(PORT, () => {
console.log(`Server đang chạy tại http://localhost:${PORT}`);
console.log(`Node.js version: ${process.version}`); // In ra version Node để lát so sánh trong container
});
JavaScript- Mở file
package.json
và chắc chắn rằng có phầnscripts
để khởi động server (nếunpm init -y
chưa tạo):
// package.json
{
// ... các thông tin khác
"main": "server.js", // Đảm bảo trỏ đúng file chính
"scripts": {
"start": "node server.js", // Lệnh để chạy app
"test": "echo \"Error: no test specified\" && exit 1"
},
// ...
}
JSON- Chạy thử trên máy local: Mở terminal trong thư mục
my-node-app
và chạynode server.js
hoặcnpm start
. Sau đó mở trình duyệt truy cậphttp://localhost:3000
. Nếu thấy chữ “Hello Docker!…” là ứng dụng đã chạy ngon lành. NhớCtrl + C
để dừng server lại nhé.
Ok, ứng dụng mẫu đã sẵn sàng. Giờ đến phần chính!
Viết Dockerfile đầu tiên
Trong thư mục gốc của dự án (my-node-app/
), tạo một file mới tên là Dockerfile
(không có đuôi mở rộng) và nhập nội dung sau. Mình sẽ giải thích từng dòng một cách cặn kẽ:
# Bước 1: Chọn Base Image (Ảnh nền)
# Sử dụng image Node.js chính thức từ Docker Hub.
# Chọn phiên bản LTS (Long Term Support) gần nhất, dùng base Alpine Linux cho nhẹ.
FROM node:18-alpine
# Bạn có thể thay 18 bằng phiên bản LTS khác nếu muốn (vd: 20-alpine)
# Bước 2: Đặt Thư Mục Làm Việc (Working Directory) bên trong Container
# Tạo thư mục /app và chuyển vào đó. Các lệnh sau sẽ chạy trong thư mục này.
WORKDIR /app
# Bước 3: Sao Chép File package.json (và package-lock.json nếu có)
# Copy file quản lý dependencies vào trước.
# Tối ưu caching: Bước này chỉ chạy lại nếu file package*.json thay đổi.
COPY package*.json ./
# Bước 4: Cài Đặt Dependencies
# Chạy npm install bên trong image để cài các thư viện cần thiết.
# Lớp này sẽ được cache và chỉ chạy lại nếu bước COPY package*.json ở trên chạy lại.
RUN npm install
# Lưu ý: Trong thực tế có thể dùng `RUN npm ci --only=production` để cài đặt nhanh hơn và chỉ cài production dependencies. Nhưng để đơn giản, ta dùng `npm install`.
# Bước 5: Sao Chép Toàn Bộ Source Code
# Copy code của ứng dụng (server.js và các file khác nếu có) vào thư mục /app.
# Copy sau khi npm install để tận dụng cache. Bước này chạy lại khi code thay đổi,
# nhưng không cần chạy lại npm install nếu package.json không đổi.
COPY . .
# Bước 6: Expose Port (Thông báo cổng ứng dụng lắng nghe)
# Khai báo rằng ứng dụng bên trong container sẽ lắng nghe ở port 3000.
# Lưu ý: Bước này chỉ là thông tin, chưa thực sự mở port ra máy host.
EXPOSE 3000
# Bước 7: Lệnh Khởi Chạy Container (Default Command)
# Chỉ định lệnh sẽ được thực thi khi container khởi động từ image này.
# Dùng dạng JSON array ["executable", "param1", ...] là best practice.
CMD [ "node", "server.js" ]
# Hoặc có thể dùng CMD [ "npm", "start" ] nếu bạn đã định nghĩa script "start" trong package.json
Dockerfile
Trong đó:
FROM node:18-alpine
:FROM
: Luôn là chỉ dẫn đầu tiên, xác định image nền tảng mà chúng ta sẽ xây dựng dựa trên đó. Giống như chọn loại móng nhà vậy.node
: Tên image chính thức của Node.js trên Docker Hub.:18-alpine
: Đây là tag, chỉ định phiên bản.18
là phiên bản Node.js.alpine
là biến thể dựa trên Alpine Linux – một bản Linux siêu nhẹ, giúp image cuối cùng của chúng ta nhẹ hơn đáng kể so với các bản dựa trên Debian/Ubuntu thông thường. Rất được ưa chuộng!
WORKDIR /app
:- Tạo ra thư mục
/app
bên trong container (nếu chưa có) và đặt nó làm thư mục làm việc hiện hành cho các lệnh tiếp theo (COPY
,RUN
,CMD
). Giống như bạncd /app
vậy đó. Điều này giúp các đường dẫn sau đó gọn gàng hơn.
- Tạo ra thư mục
COPY package*.json ./
:COPY
: Sao chép file hoặc thư mục từ máy host (chính là thư mục chứa Dockerfile – gọi là build context) vào bên trong image.package*.json
: Sao chép cảpackage.json
vàpackage-lock.json
(nếu có). Dấu*
là ký tự đại diện../
: Đích đến là thư mục làm việc hiện tại bên trong image (là/app
doWORKDIR
ở trên).- Tại sao copy cái này trước? TỐI ƯU CACHE! Docker xây dựng image theo từng lớp (layer). Nếu nội dung của một lớp không thay đổi (ở đây là file
package*.json
), Docker sẽ tái sử dụng lớp đó từ cache trong các lần build sau, không cần chạy lại. Bằng cách copypackage*.json
và chạynpm install
trước khi copy code nguồn, chúng ta đảm bảo rằngnpm install
chỉ chạy lại khi nào dependencies thực sự thay đổi, chứ không phải mỗi khi ta sửa một dòng codeserver.js
. Tiết kiệm cực nhiều thời gian build!
RUN npm install
:RUN
: Thực thi một lệnh command-line trong quá trình build image. Lệnh này chạy bên trong môi trường tạm thời của image đang được xây dựng.npm install
: Lệnh quen thuộc để cài đặt các dependencies được định nghĩa trongpackage.json
. Kết quả (thư mụcnode_modules
) sẽ được lưu vào một lớp mới của image.
COPY . .
:- Sao chép tất cả các file và thư mục còn lại từ build context (máy host) vào thư mục làm việc hiện tại (
/app
) bên trong image. Bao gồmserver.js
và bất kỳ file nào khác bạn có. - Tại sao copy code sau
npm install
? Cũng là để tận dụng cache. Nếu bạn chỉ sửa codeserver.js
mà không thay đổipackage.json
, khi build lại, Docker sẽ dùng lại cache cho lớpFROM
,WORKDIR
,COPY package*.json
,RUN npm install
, và chỉ chạy lại lớpCOPY . .
này và các lớp sau nó. Nhanh hơn nhiều!
- Sao chép tất cả các file và thư mục còn lại từ build context (máy host) vào thư mục làm việc hiện tại (
EXPOSE 3000
:- Đây là một chỉ dẫn mang tính thông báo (documentation). Nó cho Docker biết rằng container chạy từ image này sẽ lắng nghe các kết nối đến trên port
3000
(port mà ứng dụng Node.js của chúng ta đang listen). - Quan trọng:
EXPOSE
không tự động mở port này ra thế giới bên ngoài hay máy host của bạn. Nó chỉ là metadata. Để thực sự truy cập được port này từ máy host, chúng ta cần dùng tùy chọn-p
khi chạydocker run
(sẽ thấy ở bước sau).
- Đây là một chỉ dẫn mang tính thông báo (documentation). Nó cho Docker biết rằng container chạy từ image này sẽ lắng nghe các kết nối đến trên port
CMD [ "node", "server.js" ]
:CMD
: Chỉ định lệnh mặc định sẽ được thực thi khi container bắt đầu chạy từ image này. Mỗi Dockerfile chỉ nên có mộtCMD
hiệu lực (cái cuối cùng).["node", "server.js"]
: Đây là dạng exec form (dạng JSON array). Docker sẽ thực thi trực tiếp filenode
với tham số làserver.js
. Dạng này thường được khuyên dùng hơn dạng shell form (CMD node server.js
).- Khác
RUN
:RUN
chạy lúc build image để cài đặt, cấu hình.CMD
chạy lúc start container để khởi động ứng dụng chính.

Phù! Hơi dài nhưng hiểu rõ từng lệnh này là cực kỳ quan trọng đó. Giờ thì build thôi!
Build Docker Image – Tạo Khuôn Mẫu
- Mở terminal hoặc command prompt của bạn, đảm bảo bạn đang ở trong thư mục gốc của dự án (
my-node-app/
, nơi chứaDockerfile
). - Chạy lệnh sau:
docker build -t my-node-app:1.0 .
Bash- Giải thích lệnh:
docker build
: Lệnh dùng để build một image từ Dockerfile.-t my-node-app:1.0
: Option-t
(viết tắt của--tag
) dùng để đặt tên và tag cho image theo định dạngrepository:tag
.my-node-app
: Tên của image (repository). Bạn có thể đặt tên khác, ví dụsiucode/my-first-node-app
.1.0
: Tag (thường là phiên bản). Nếu không chỉ định tag, mặc định làlatest
. Đặt tag rõ ràng là thực hành tốt.
.
(dấu chấm ở cuối): Rất quan trọng! Nó chỉ định build context. Docker sẽ gửi toàn bộ nội dung của thư mục này (và các thư mục con) đến Docker Daemon để thực hiện quá trình build. Dockerfile cũng phải nằm trong thư mục này.
- Quan sát output: Bạn sẽ thấy Docker thực hiện từng bước (Step 1/7, Step 2/7…) tương ứng với các chỉ dẫn trong
Dockerfile
. Nếu bạn build lần thứ hai mà không thay đổi gì, bạn sẽ thấy chữ(cached)
hiện ra ở nhiều bước, cho thấy Docker đang dùng lại cache rất nhanh.

- Kiểm tra image đã tạo: Sau khi build thành công, gõ lệnh
docker images
hoặcdocker image ls
để xem danh sách các image trên máy bạn. Bạn sẽ thấy imagemy-node-app
với tag1.0
xuất hiện. Chú ý cộtSIZE
để thấy image Alpine nhẹ như thế nào nhé!
Chạy Docker Container – Đúc bánh từ khuôn
Image đã có, giờ là lúc tạo ra một container chạy ứng dụng của chúng ta từ image đó:
- Chạy lệnh sau trong terminal:
docker run -p 8080:3000 --name my-running-app -d my-node-app:1.0
Bash- Giải thích lệnh:
docker run
: Lệnh để tạo và khởi chạy một container mới từ một image.-p 8080:3000
: Port Mapping! Đây là phần quan trọng để “mở cổng” từ máy host vào container.8080
: Port trên máy host mà bạn muốn sử dụng để truy cập container. Bạn có thể chọn port khác nếu 8080 đã bị dùng.3000
: Port bên trong container mà ứng dụng đang lắng nghe (chính là port taEXPOSE
và dùng trongserver.js
).- Cú pháp là
hostPort:containerPort
. Docker sẽ chuyển tiếp mọi request đếnlocalhost:8080
trên máy host vào port3000
của container.
--name my-running-app
: Đặt một cái tên cụ thể (my-running-app
) cho container đang chạy này. Giúp chúng ta dễ dàng quản lý (stop, remove, xem log…) bằng tên thay vì ID dài ngoằng. Nếu không có option này, Docker sẽ tự tạo tên ngẫu nhiên.-d
(hoặc--detach
): Chạy container ở chế độ detached (chạy nền). Terminal của bạn sẽ được trả về ngay sau khi container khởi động, thay vì bị “kẹt” lại để hiển thị log của container. Rất hữu ích khi chạy server.my-node-app:1.0
: Tên và tag của image mà chúng ta muốn chạy. Docker sẽ tìm image này trên máy bạn để tạo container.
- Kiểm tra container đang chạy: Gõ lệnh
docker ps
(hoặcdocker container ls
). Bạn sẽ thấy containermy-running-app
đang ở trạng tháiUp
và thông tin về port mapping (0.0.0.0:8080->3000/tcp
). - (Tùy chọn) Xem log của container: Nếu bạn chạy với
-d
, bạn có thể xem log (output từconsole.log
trongserver.js
) bằng lệnh:docker logs my-running-app
. Bạn sẽ thấy dòng “Server đang chạy tại http://localhost:3000” và phiên bản Node.js bên trong container (sẽ là Node 18 như trong base image).

Kiểm tra thành quả!
Đây là giây phút sung sướng nhất! Mở trình duyệt web của bạn và truy cập vào địa chỉ:
http://localhost:8080
(Nhớ là port 8080 của máy host nhé!)
Bạn sẽ thấy dòng chữ: Hello Docker! Chào mừng bạn đến với container đầu tiên.
Thử truy cập tiếp: http://localhost:8080/api/users
Bạn sẽ thấy dữ liệu JSON mẫu: [{"id":1,"name":"Siucode User 1"},{"id":2,"name":"Docker Fan"}]
Bạn đã thành công “Docker hóa” ứng dụng Node.js đầu tiên của mình! Ứng dụng của bạn giờ đang chạy bên trong một môi trường biệt lập, được quản lý bởi Docker.
Dọn dẹp chiến trường
Sau khi thử nghiệm xong, bạn có thể dọn dẹp để tránh lãng phí tài nguyên:
- Dừng container đang chạy:
docker stop my-running-app
Bash- Xóa container đã dừng: (Container phải được stop trước khi xóa)
docker rm my-running-app
Bash(Bạn có thể kết hợp 2 lệnh trên bằng docker rm -f my-running-app
để ép xóa container đang chạy, nhưng nên stop
trước cho an toàn)
- Xóa image: Nếu bạn không cần image này nữa:
docker rmi my-node-app:1.0
Bash(Lưu ý: Chỉ xóa được image nếu không có container nào (kể cả container đã stop) đang sử dụng nó)
Wow, xong rồi! Ở bài này, chúng ta đã làm được một việc rất quan trọng: đóng gói và chạy backend Node.js bằng Docker. Trong Phần 4, chúng ta sẽ tiếp tục thực hành với việc “Docker hóa” ứng dụng Frontend React và giới thiệu một kỹ thuật rất hay để tối ưu image cho frontend: Multi-stage build. Sẽ còn nhiều điều thú vị phía trước!
Lời kết
Qua bài thực hành đầu tiên này, hy vọng anh em đã hình dung rõ hơn về quy trình làm việc cơ bản với Docker: Viết Dockerfile
-> docker build
-> docker run
. Đặc biệt là cách các chỉ dẫn trong Dockerfile
hoạt động và tầm quan trọng của việc tối ưu cache. Ban đầu có thể hơi nhiều lệnh, nhưng khi đã quen thì sẽ thấy rất nhanh và tiện lợi. Đừng ngần ngại thử lại các bước trên hoặc chỉnh sửa Dockerfile
để thử nghiệm nhé!
Hẹn gặp lại các bạn ở phần tiếp theo.
SiuCode – Vừa code vừa siuuuu 🚀