Viết Dockerfile cho app web: ví dụ từng bước cho người mới
G
GnurtLập trình viên full-stack
12 phút đọc
Trong bài này
Muốn app web của bạn chạy được ở bất cứ đâu, bạn cần đóng gói nó lại, và Dockerfile chính là tờ giấy ghi cách đóng gói đó. Trong bài này, bạn sẽ viết Dockerfile đầu tiên cho một máy chủ web nhỏ viết bằng Node.js (dùng Express), rồi tự build nó thành Image và chạy thử một Container trên máy của mình. Khi app chạy lên, bạn mở trình duyệt vào địa chỉ của nó sẽ thấy lời chào “Xin chào từ Docker!”.
Trước khi bắt đầu, bạn kiểm tra xem Docker đã có trên máy chưa bằng lệnh sau trong Terminal:
docker --version
Nếu Terminal in ra một dòng như thế này thì Docker đã có trên máy bạn. Số phiên bản và mã build trên máy bạn sẽ khác, chỉ cần dòng đó bắt đầu bằng Docker version là được:
Docker version 28.3.2, build 578ccf6
Nếu Terminal báo không tìm thấy lệnh docker — trên Windows, câu báo lỗi bắt đầu bằng ‘docker’ is not recognized as an internal or external command…; trên macOS là command not found: docker — thì bạn cài đặt Docker theo hướng dẫn của Docker. Còn nếu lệnh docker --version chạy được mà lệnh docker khác lại báo không kết nối được, nghĩa là Docker Desktop chưa mở — bạn mở Docker Desktop lên, đợi nó khởi động xong rồi gõ lại lệnh.
Nếu ví việc tạo Image như nấu một món ăn, thì Dockerfile là tờ công thức: mỗi dòng ghi một việc phải làm, và Docker cứ thế làm lần lượt từ dòng đầu tới dòng cuối.
Mỗi khi một bước làm thay đổi file, như chép file vào hay chạy lệnh cài gói, Docker giữ lại kết quả của bước đó thành một lớp. Các lớp chồng lên nhau, và cả chồng lớp đó chính là Image.
Lần build sau, bước nào không đổi thì Docker lấy lại kết quả cũ, khỏi chạy lại từ đầu. Chỉ khi bạn sửa một bước, Docker mới chạy lại từ bước đó trở đi. Chính vì vậy mà thứ tự các dòng trong Dockerfile rất quan trọng. Lát nữa, khi viết file, bạn sẽ thấy điều này quyết định chỗ đặt từng dòng.
Các chỉ dẫn như RUN và COPY tạo ra các lớp của Image.
Viết Dockerfile cho app web
Giờ mình bắt tay vào viết. App ví dụ rất nhỏ, nên bạn chỉ cần vài file là đủ.
App ví dụ gồm những file gì
App ví dụ chỉ gồm hai file chính: package.json khai báo tên app và các gói cần dùng, còn server.js chạy một máy chủ web nhỏ. Khi làm xong cả bài, thư mục dự án của bạn sẽ trông như thế này:
Trong bài này, bạn sẽ tạo 4 file: package.json, server.js, Dockerfile và .dockerignore. Còn .env và docker-compose.yml trong cây thư mục thì thuộc về bài Compose, bạn chưa cần động tới chúng bây giờ.
Tạo các file của app
Bạn tạo một thư mục mới tên my-app rồi mở cửa sổ lệnh trong thư mục đó, vì mọi file và mọi lệnh trong bài đều nằm ở đây:
mkdir my-app
cd my-app
Cách mở cửa sổ lệnh ngay trong thư mục my-app: trên Windows, bạn bấm chuột phải vào khoảng trống trong thư mục rồi chọn Open in Terminal; không thấy mục đó thì bạn gõ cmd vào thanh địa chỉ của File Explorer rồi nhấn Enter. Trên macOS, bạn bấm chuột phải vào thư mục my-app trong Finder rồi chọn New Terminal at Folder; nếu không thấy mục đó, bạn bật nó trong phần Services của cài đặt bàn phím, nhóm Files and Folders, hoặc mở Terminal rồi gõ lệnh cd tới thư mục đó.
Từ đây trở đi, mọi lệnh trong bài đều chạy trong thư mục my-app, nên mỗi lần mở cửa sổ lệnh mới, bạn cd vào my-app trước rồi mới gõ lệnh.
Bạn tạo các file bằng chương trình soạn thảo văn bản thuần túy trên máy của mình: Notepad trên Windows, TextEdit trên macOS (bạn nhớ chuyển sang chế độ văn bản thuần túy trước khi gõ: menu Format → Make Plain Text, hoặc phím tắt Shift + Command + T), hay VS Code nếu bạn đã cài sẵn.
Trên Windows, khi lưu file Dockerfile, bạn nhớ chọn lưu dạng All files (hoặc bật hiện đuôi file trong File Explorer để kiểm tra). Nếu không, file của bạn sẽ thành Dockerfile.txt, và Docker không nhận file có đuôi .txt.
File này khai báo app tên web-app, chạy từ file server.js, và dùng hai gói: express để chạy máy chủ web, còn mysql2 để kết nối MySQL.
Tiếp theo là file server.js:
const express = require("express");
const mysql = require("mysql2/promise");
const app = express();
app.get("/", async (req, res) => {
if (!process.env.DB_HOST) {
return res.send("Xin chào từ Docker!");
}
try {
const conn = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
const [rows] = await conn.query("SELECT NOW() AS now");
await conn.end();
res.send("Xin chào từ Docker! Giờ trên MySQL: " + rows[0].now);
} catch (err) {
res.status(500).send("Chưa kết nối được database: " + err.message);
}
});
app.listen(3000, () => console.log("App chạy ở cổng 3000"));
Biến môi trường là cách đưa cấu hình vào app từ bên ngoài, thay vì ghi cứng trong code. Ở đây, app nhìn vào biến môi trường để biết có thông tin database không: nếu có, nó kết nối MySQL và trả về giờ hiện tại từ database; nếu không, nó chỉ trả về lời chào. Bạn không cần hiểu hết từng dòng code. Chỉ cần biết file này nhận yêu cầu ở cổng 3000 và đọc cấu hình từ biến môi trường.
Viết Dockerfile
Giờ đến phần chính. Bạn tạo một file tên Dockerfile (không có đuôi) trong cùng thư mục, với 7 dòng sau:
FROM node:26-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
FROM node:26-alpine: lấy Image nền node:26-alpine, một Image đã cài sẵn Node.js 26, làm điểm bắt đầu cho mọi bước sau.
WORKDIR /app: tạo thư mục /app trong Image rồi đặt nó làm thư mục làm việc, để các lệnh sau đều chạy ở đó.
COPY package*.json ./: chép package.json vào Image; dấu sao để chép luôn cả package-lock.json nếu có file đó.
RUN npm install: chạy lệnh cài các gói đã khai báo trong package.json, ngay lúc build, nên khi build xong, Image đã có đủ thứ app cần.
COPY . .: chép nốt phần code còn lại vào Image.
EXPOSE 3000: ghi chú rằng app nghe ở cổng 3000. Đây chỉ là ghi chú, nó không tự mở cổng ra ngoài. Muốn mở cổng thì phải dùng -p lúc chạy, lát nữa bạn sẽ thấy.
CMD ["node", "server.js"]: lệnh chạy mỗi khi Container khởi động: mở app bằng Node.js với file server.js.
Vì sao không chép hết code ngay từ đầu cho gọn? Nhờ cơ chế lớp ở trên: bước npm install là bước tốn thời gian nhất, mà nó chỉ phụ thuộc vào package.json. Chép package.json riêng trước, thì lần sau bạn chỉ sửa code, Docker sẽ dùng lại kết quả của bước npm install và chỉ chạy lại từ bước COPY . . trở đi. Còn nếu chép hết code ngay từ đầu, mỗi lần sửa một dòng code bạn lại phải chờ cài lại toàn bộ gói.
File .dockerignore
Trong cùng thư mục my-app, bên cạnh Dockerfile, bạn cần tạo thêm file .dockerignore. File này liệt kê những thứ Docker không được chép vào Image:
node_modules
npm-debug.log
.git
.env
Trong bài này, trên máy bạn chưa có thư mục node_modules. Nó chỉ xuất hiện nếu bạn chạy npm install trên máy của mình. Nhưng file này vẫn cần có từ bây giờ: chỉ cần một lần bạn chạy npm install trên máy, thư mục node_modules (nơi chứa các gói đã cài) sẽ xuất hiện, và lần build sau lệnh COPY . . sẽ chép cả nó vào Image nếu không có file loại trừ. Hai dòng còn lại cũng vậy: npm-debug.log là file log khi npm gặp lỗi, còn .git là thư mục lưu lịch sử thay đổi của code. Cả hai đều không cần có trong Image. Thiếu file .dockerignore, lệnh COPY . . sẽ chép cả thư mục node_modules nặng nề vào Image, làm Image nặng hơn và build chậm hơn mà không được gì thêm.
File .env sẽ giữ mật khẩu database trong bài Compose. Mật khẩu không bao giờ được nằm trong Image. Docker Compose sẽ đọc nó từ file .env trên máy của bạn và đưa vào Container lúc chạy.
Build Image và chạy thử
Có Dockerfile rồi, giờ mình build nó thành Image. Bạn mở cửa sổ lệnh tại thư mục dự án (nơi có Dockerfile) rồi chạy:
docker build -t web-app .
Lệnh này build Image từ Dockerfile trong thư mục hiện tại. Chữ -t là viết tắt của --tag, tức là bạn đặt tên cho Image, ở đây là web-app; vì bạn không ghi số phiên bản sau tên nên Docker mặc định gắn nhãn latest. Dấu chấm ở cuối nghĩa là lấy thư mục hiện tại làm nguồn để build. Khi build, Docker in từng bước theo thứ tự, bước nào xong thì báo xong; lần build đầu có thể mất một lúc vì Docker phải tải Image nền node:26-alpine về máy. Build xong, Docker báo đã tạo xong Image tên web-app.
Build xong, bạn chạy thử một Container từ Image đó:
docker run --rm --init -p 3000:3000 --name web web-app
--rm: tự xóa Container sau khi dừng, để lần sau chạy lại không bị trùng tên.
--init: nhờ Docker chèn một chương trình init nhỏ (tini) làm tiến trình số 1 trong Container. Nhờ có nó mà tín hiệu dừng như Ctrl+C mới tới được app, và các tiến trình con đã kết thúc được dọn sạch.
-p 3000:3000: ánh xạ cổng 3000 trên máy bạn sang cổng 3000 trong Container. Số đầu là cổng trên máy bạn, số sau là cổng trong Container.
--name web: đặt tên Container là web.
web-app: tên Image cần chạy, chính là Image bạn vừa build.
Chừng nào app còn chạy, cửa sổ lệnh này sẽ bận, bạn không gõ thêm lệnh khác vào đó được. Muốn gõ lệnh khác thì bạn mở thêm một cửa sổ lệnh mới.
Bây giờ bạn mở trình duyệt và vào localhost:3000, bạn sẽ thấy dòng chữ “Xin chào từ Docker!”.
App chỉ chào vậy thôi, vì bạn chưa đặt biến môi trường nào cho nó nên đoạn code nối MySQL không chạy. Đoạn đó sẽ chạy ở bài Compose, khi các biến môi trường đã có đủ.
Muốn dừng app, bạn chỉ cần bấm Ctrl+C trong cửa sổ lệnh đang chạy app, hoặc mở một cửa sổ lệnh khác và chạy docker stop web. Chữ web ở đây chính là tên bạn đã đặt bằng --name web ở lệnh chạy. Quên tên Container thì bạn chạy docker ps để xem tên và mã định danh (ID) của các Container đang chạy. Bạn cũng có thể dừng Container bằng mã định danh thay cho tên, ví dụ docker stop <ID>. Lệnh docker stop không cần chạy trong thư mục my-app; chỉ hai lệnh build và run mới cần chạy đúng thư mục dự án.
Lỗi thường gặp khi mới viết Dockerfile
Làm theo các bước trên, bạn có thể gặp ba lỗi sau. Cách sửa đều ngắn, nên bạn cứ đối chiếu với điều mình gặp rồi làm theo.
Quên tạo .dockerignore. Docker chép cả node_modules và các file thừa vào Image, làm Image nặng và build chậm. Bạn tạo file .dockerignore với đúng 4 dòng như trên rồi build lại.
Lệnh COPY sai đường dẫn. Docker báo lỗi, thường có chữ COPY failed kèm tên file nó không tìm thấy. Bạn kiểm tra lại đường dẫn trong lệnh COPY, và chắc chắn mình đang chạy lệnh trong đúng thư mục dự án, nơi có Dockerfile.
Cổng 3000 đã bị chương trình khác dùng. Khi chạy lệnh run, Docker báo lỗi và Container không khởi động được. Bạn sẽ thấy câu lỗi có đoạn Bind for 0.0.0.0:3000 failed: port is already allocated hoặc bind: address already in use. Cả hai đều nghĩa là cổng 3000 trên máy bạn đã có chương trình khác dùng. Cách nhanh nhất là đổi sang cổng khác: thay số đầu trong -p 3000:3000 thành 3001:3000 (cổng 3001 trên máy bạn trỏ tới cổng 3000 trong Container), chạy lại lệnh, rồi mở trình duyệt ở localhost:3001 thay vì localhost:3000. Nếu biết chương trình nào đang chiếm cổng thì bạn tắt nó đi cũng được.
Câu hỏi thường gặp
Dockerfile có phải là Image không?
Không. Dockerfile là file văn bản ghi các chỉ dẫn; Image là kết quả Docker tạo ra sau khi làm theo từng chỉ dẫn trong file đó.
Kết luận
Xong bài này, bạn đã có Dockerfile đầu tiên của mình, một Image mang tên web-app và một Container chạy được app đó trên máy của bạn.
Bài tiếp theo trong loạt bài này sẽ lấy chính app và Dockerfile này để chạy cả app và MySQL cùng lúc bằng Docker Compose, đồng thời giữ dữ liệu trong Volume để dữ liệu không mất khi Container dừng: Docker Compose cho app web kèm database.
Nguồn tham khảo
Các liên kết dưới đây đã được kiểm chứng ngày 05/11/2026.
Bạn có thể sử dụng tài khoản Google để bình luận. Google chỉ chia sẻ tên và địa chỉ email đã xác minh để hiển thị tên của bạn. Bạn có thể đăng xuất bất cứ lúc nào.
Ở bài trước, bạn đã viết Dockerfile cho một app web nhỏ, build thành Image và chạy thử một Container. Bài này dùng chính app và Dockerfile đó để chạy cả app và MySQL cùng lúc bằng Docker Compose, chỉ với một lệnh (docker compose up). Bạn cần có sẵn Docker; nếu chưa cài thì...
Khi làm việc với Docker, bạn sẽ thường xuyên gặp bốn khái niệm: Image, Container, Volume và Network. Hiểu đơn giản, Image là bản thiết kế, Container là Image đang chạy. Volume giữ dữ liệu, còn Network kết nối các Container với nhau. Bốn khái niệm này đi theo một thứ tự: có bản thiết...
Docker là gì? Docker là một nền tảng mở giúp bạn đóng gói, vận chuyển và chạy ứng dụng. Ứng dụng chạy trong một môi trường tách biệt gọi là container. Nói ngắn gọn, Docker giúp một ứng dụng chạy giống nhau trên nhiều máy khác nhau. Mọi thứ ứng dụng cần đều được đóng...
Bình luận
Bạn có thể sử dụng tài khoản Google để bình luận. Google chỉ chia sẻ tên và địa chỉ email đã xác minh để hiển thị tên của bạn. Bạn có thể đăng xuất bất cứ lúc nào.
Đăng nhập bằng GoogleChưa có bình luận nào. Hãy là người đầu tiên!