Buổi 09 – Dockerfile
Tuần 5 · Giai đoạn 2 – Containerization · Slide: Slide buổi 09
Tóm tắt: Viết, build và chạy Dockerfile cho Node.js API của CLB; hiểu build context, layer cache, USER, CMD và ENTRYPOINT, và chẩn đoán lỗi build do sai context.
Mục tiêu học tập
- Giải thích được vai trò của từng chỉ thị
FROM,WORKDIR,COPY,RUN,ENV,EXPOSE,CMD,ENTRYPOINTvà khác biệt giữa build time với run time. - Viết được Dockerfile cho
starter-project/backend, build ra imageclub-api:lab9và chạy được endpoint/healthqua port mapping. - Giải thích được build context và
.dockerignore, chứng minh được file không mong muốn (ví dụ.env) có thể lọt vào image. - Sắp xếp được thứ tự các chỉ thị để tận dụng layer cache, quan sát được
CACHEDkhi chỉ sửa mã nguồn. - Phân biệt được
CMDvàENTRYPOINT, exec form và shell form, và ghi đè được chúng khidocker run. - Chẩn đoán được lỗi build do chạy
docker buildsai thư mục (Tình huống 05) và sửa đúng nguyên nhân gốc.
Điều kiện cần
- Đã học Buổi 07 và 08: chạy được
docker run, hiểu image, container, port mapping,docker logs,docker exec. - Docker Desktop đang chạy với WSL2 backend; làm việc trong terminal WSL2 Ubuntu, trong filesystem của WSL (thư mục dạng
~/..., không dùng/mnt/c). - Đã clone repo giáo trình vào WSL, ví dụ
~/devops-course(cóstarter-project/backendvàexamples/docker/hello-node). - Repo cá nhân
devops-labsđã có trên máy (clone về~/devops-labs), sẽ chứa thư mụclab-09/. - Kiểm tra nhanh:
docker version --format '{{.Server.Version}}'
docker run --rm hello-world | head -n 3
ls ~/devops-course/starter-project/backendKết quả mong đợi (rút gọn): in ra phiên bản Docker Engine (27 trở lên), thông điệp "Hello from Docker!", và danh sách có app.js, server.js, package.json.
Vấn đề thực tế
Backend API của CLB chạy tốt trên máy bạn Hùng với Node.js 22. Bạn Lan clone về, dùng Node 18 và npm install ra bộ thư viện khác, API lỗi ngay lúc khởi động. Mentor muốn đưa API lên máy chung của CLB thì phải cài đúng Node, đúng thư viện, đúng biến môi trường, mỗi lần một kiểu. Buổi 07 bạn đã biết container giải quyết bài toán này, nhưng container chỉ chạy được khi có image. Thứ mô tả cách tạo image đó chính là Dockerfile.
Dockerfile là một file văn bản, nằm cùng mã nguồn, ghi lại công thức: bắt đầu từ runtime nào, chép gì vào, cài gì, chạy lệnh nào khi start. Có file này trong Git, ai cũng build ra image giống nhau, CI/CD cũng dùng lại đúng công thức đó (Buổi 13 trở đi). Hôm nay bạn viết Dockerfile đầu tiên cho club-api và học các lỗi kinh điển: build chậm vì mất cache, image chứa cả file bí mật, chạy bằng root, container không dừng gọn.
Khái niệm cốt lõi
Dockerfile là công thức, image là sản phẩm
Mỗi chỉ thị trong Dockerfile tạo ra một layer (lớp chỉ đọc) xếp chồng lên nhau. Image là chồng các layer đó cộng metadata (lệnh mặc định, biến môi trường, port khai báo...). Container là image cộng một lớp ghi mỏng ở trên cùng.
| Chỉ thị | Làm gì | Chạy lúc nào | Ghi chú quan trọng |
|---|---|---|---|
FROM | Chọn base image làm nền | Build | Luôn ghim phiên bản, ví dụ node:22-alpine, không dùng latest |
WORKDIR | Đặt thư mục làm việc, tạo nếu chưa có | Build và run | Dùng thay cho RUN cd ... |
COPY | Chép file từ build context vào image | Build | Nguồn tính theo build context, không phải theo vị trí Dockerfile |
RUN | Chạy lệnh trong lúc build, kết quả thành layer | Build | Dùng để cài dependency: npm ci |
ENV | Đặt biến môi trường, tồn tại cả lúc container chạy | Build và run | Không đặt secret ở đây |
EXPOSE | Khai báo (tài liệu) port ứng dụng lắng nghe | Metadata | Không mở port ra host; muốn truy cập phải dùng -p |
USER | Đổi user chạy các lệnh sau đó và container | Build và run | Image node có sẵn user node (uid 1000) |
CMD | Lệnh mặc định khi start container | Run | Bị thay thế bởi tham số cuối của docker run |
ENTRYPOINT | Chương trình cố định của container | Run | Tham số của docker run được nối thêm vào sau |
Build context
Khi chạy docker build -t tên ., dấu . là build context: thư mục mà Docker CLI gói lại và gửi cho builder. COPY chỉ thấy các file nằm trong context. Hai hệ quả:
- Chạy
docker buildở sai thư mục thìCOPY package*.json ./không tìm thấy file, hoặc không tìm thấy Dockerfile. - Mọi thứ trong context (kể cả
.env,.git,node_modules) đều có thể bịCOPY . .đưa vào image. File.dockerignoređặt trong thư mục context liệt kê những thứ cần loại trừ.
Layer cache
Docker build từng chỉ thị theo thứ tự. Nếu chỉ thị và dữ liệu đầu vào của nó (ví dụ nội dung file được COPY) không đổi so với lần build trước, Docker dùng lại layer cũ và in CACHED. Khi một layer bị vô hiệu, mọi layer phía sau nó cũng build lại. Quy tắc thực tế: đặt thứ ít thay đổi lên trên (base image, package.json, npm ci), thứ hay thay đổi xuống dưới (mã nguồn).
CMD, ENTRYPOINT, exec form và shell form
| Tiêu chí | CMD | ENTRYPOINT |
|---|---|---|
| Ý nghĩa | Lệnh (hoặc tham số) mặc định | Chương trình chính của container |
docker run image abc | Thay thế hoàn toàn bằng abc | Nối abc vào sau ENTRYPOINT |
| Ghi đè khi cần | Truyền lệnh sau tên image | Phải dùng --entrypoint |
| Dùng khi | Ứng dụng thông thường, muốn dễ chạy lệnh khác để debug | Image là một "công cụ" luôn chạy một chương trình |
Hai cách viết: exec form CMD ["node", "server.js"] (mảng JSON, chạy trực tiếp không qua shell) và shell form CMD node server.js (chạy qua /bin/sh -c). Hãy dùng exec form cho ứng dụng: process của bạn là PID 1 và nhận tín hiệu từ docker stop trực tiếp. Có thể kết hợp: ENTRYPOINT ["node"] và CMD ["server.js"], khi đó docker run image --version chạy node --version.
Kiến trúc và cách hoạt động
Giải thích từng bước:
- Docker CLI nén thư mục context (trừ các mục trong
.dockerignore) và gửi cho builder. Context càng lớn, bước này càng chậm. - Builder đọc Dockerfile, bắt đầu từ
FROM: kéo base imagenode:22-alpinevề nếu chưa có. - Mỗi
COPY,RUNtạo một layer mới. Builder so sánh với cache để quyết định build lại hay dùngCACHED. - Các chỉ thị
ENV,EXPOSE,USER,CMDchỉ ghi metadata vào image, không tạo ra dữ liệu mới. docker runtạo container từ image, đặt lớp ghi lên trên và chạyCMD(hoặcENTRYPOINTcộngCMD).
Demo của mentor
Mục đích: cho cả lớp thấy cùng một ứng dụng nhỏ, hai kết quả khác nhau chỉ vì cách viết Dockerfile. Dùng examples/docker/hello-node/. Mentor dùng port 3100 để không đụng với lớp.
Bước 1. Chuẩn bị bản sao để sửa thoải mái.
mkdir -p ~/demo && cp -r ~/devops-course/examples/docker/hello-node ~/demo/ && cd ~/demo/hello-node
cat Dockerfile.naiveKết quả mong đợi: Dockerfile ngắn 6 dòng hiệu lực: FROM node:22, WORKDIR /app, COPY . ., RUN npm install, EXPOSE 3000, CMD.
Bước 2. Build bản "ngây thơ" và chạy.
docker build -f Dockerfile.naive -t hello-node:naive .
docker run -d --name demo-naive -p 3100:3000 hello-node:naive
curl -s localhost:3100/healthKết quả mong đợi: build mất vài chục giây (lần đầu kéo image), curl in {"status":"healthy"}.
Bước 3. Sửa một dòng mã nguồn rồi build lại.
echo "// sua nhe" >> server.js
docker build -f Dockerfile.naive -t hello-node:naive .Kết quả mong đợi: bước RUN npm install chạy lại từ đầu dù package.json không đổi, vì COPY . . đứng trước và nội dung context đã đổi.
Bước 4. So sánh với bản tối ưu.
docker build -f Dockerfile.optimized -t hello-node:opt .
echo "// sua nua" >> server.js
docker build -f Dockerfile.optimized -t hello-node:opt .
docker image ls hello-node
docker exec demo-naive idKết quả mong đợi: lần build thứ hai của bản opt hiện CACHED ở bước cài dependency; bảng image cho thấy naive lớn hơn nhiều opt (base Debian đầy đủ so với Alpine, cỡ vài trăm MB chênh lệch trở lên); id của bản naive in uid=0(root).
Bước 5. Dọn dẹp.
docker rm -f demo-naiveCâu hỏi mentor nên hỏi lớp:
- Nếu đổi thứ tự hai dòng
COPY package.jsonvàCOPY . ., bạn đoán lần build thứ hai nhanh hay chậm? Vì sao? - Container naive chạy bằng root: nếu API bị khai thác lỗ hổng thì kẻ tấn công có gì trong tay?
- Bản
optkhông chéptests/hay.git. Bạn nghĩ một Dockerfile tốt nên chép gì vào image?
Hands-on lab
Thời lượng 60 phút. Bạn viết Dockerfile cho club-api từ đầu, tăng dần chất lượng qua từng phần. Image đặt tên club-api:lab9, container club-api-lab9, host port 3001 (không dùng 3000 để tránh đụng với compose ở các buổi sau).
Chuẩn bị
- Tạo thư mục nộp bài và chép mã nguồn backend vào
source/:
mkdir -p ~/devops-labs/lab-09/{source,evidence,screenshots}
cd ~/devops-labs/lab-09
cp -r ~/devops-course/starter-project/backend/. source/
rm -rf source/node_modules source/Dockerfile source/.dockerignore
ls -a sourceKết quả mong đợi: thấy app.js, db.js, server.js, package.json, package-lock.json, tests. Chưa có Dockerfile: bạn sẽ tự viết. Chúng ta xóa luôn .dockerignore mẫu để bạn tự thấy vì sao nó cần.
- Bằng chứng được ghi vào
evidence/command-output.txt: thêm2>&1 | tee -a ~/devops-labs/lab-09/evidence/command-output.txtvào cuối các lệnh quan trọng (có nhắc ở từng phần; trong thư mụcsourcecó thể viết ngắn là../evidence/command-output.txt).
Phần 1. Dockerfile đầu tiên (khoảng 12 phút)
- Tạo
source/Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "server.js"]Vì sao: FROM ghim Node 22 trên Alpine (nhỏ). npm ci cài đúng phiên bản trong package-lock.json; --omit=dev bỏ thư viện dev. EXPOSE chỉ là tài liệu cho người đọc.
- Build từ thư mục
source(dấu.cuối là context):
cd ~/devops-labs/lab-09/source
docker build -t club-api:lab9 . 2>&1 | tee -a ../evidence/command-output.txtKết quả mong đợi (rút gọn): các bước FROM, WORKDIR, COPY, RUN npm ci chạy lần lượt, cuối cùng có dòng naming to docker.io/library/club-api:lab9.
- Chạy container và gọi API:
docker run -d --name club-api-lab9 -p 3001:3000 club-api:lab9
curl -s localhost:3001/health
curl -s -i localhost:3001/ready | head -n 1
docker logs club-api-lab9Kết quả mong đợi: /health trả {"status":"healthy","service":"club-api"}; /ready trả HTTP/1.1 503 Service Unavailable; log có dòng API listening on port 3000.
Vì sao /ready là 503: API cần PostgreSQL, mà container này không có database (DB_HOST mặc định là localhost, bên trong container không có gì ở port 5432). Đây là điều bình thường ở buổi này; Buổi 11 và 12 sẽ nối API với DB bằng network và Compose.
- Quan sát image:
docker image ls club-api:lab9
docker history club-api:lab9Kết quả mong đợi: dung lượng cỡ vài trăm MB trở xuống (base Alpine cộng Node và thư viện); docker history liệt kê các layer tương ứng với chỉ thị của bạn.
Phần 2. Tối ưu thứ tự để tận dụng cache (khoảng 10 phút)
- Đo lại hành vi hiện tại: sửa mã nguồn rồi build.
cd ~/devops-labs/lab-09/source
echo "// lab9 thay doi 1" >> server.js
docker build --progress=plain -t club-api:lab9 . 2>&1 | grep -E "npm ci|CACHED"Kết quả mong đợi: dòng RUN npm ci --omit=dev chạy lại (không có CACHED cạnh nó), tốn vài giây đến vài chục giây.
- Sửa Dockerfile: chép manifest riêng, cài dependency, rồi mới chép mã nguồn.
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]- Build hai lần, giữa hai lần sửa mã nguồn:
docker build --progress=plain -t club-api:lab9 . 2>&1 | grep -E "npm ci|CACHED"
echo "// lab9 thay doi 2" >> server.js
docker build --progress=plain -t club-api:lab9 . 2>&1 | tee -a ../evidence/command-output.txt | grep -E "npm ci|CACHED"Kết quả mong đợi: ở lần build thứ hai, RUN npm ci --omit=dev hiện CACHED; chỉ layer COPY . . build lại.
Vì sao: layer npm ci chỉ phụ thuộc vào hai file manifest. Mã nguồn đổi không làm manifest đổi, nên cache còn hiệu lực. Đây là thói quen quan trọng nhất của Dockerfile cho Node.js.
Phần 3. Môi trường và user an toàn (khoảng 10 phút)
- Kiểm tra container hiện tại đang chạy bằng user nào:
docker rm -f club-api-lab9
docker run -d --name club-api-lab9 -p 3001:3000 club-api:lab9
docker exec club-api-lab9 idKết quả mong đợi: uid=0(root) gid=0(root). Chạy root không cần thiết là rủi ro: nếu ứng dụng bị khai thác, kẻ tấn công có quyền cao nhất trong container.
- Cập nhật Dockerfile thành bản có
ENV,--chownvàUSER:
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]Giải thích: NODE_ENV=production làm Express và một số thư viện bật chế độ production. PORT=3000 khớp với server.js (process.env.PORT). COPY --chown=node:node để file thuộc user node. USER node đặt sau RUN npm ci để bước cài đặt vẫn có quyền ghi.
- Build lại, chạy và kiểm tra:
docker build -t club-api:lab9 .
docker rm -f club-api-lab9
docker run -d --name club-api-lab9 -p 3001:3000 club-api:lab9
docker exec club-api-lab9 id | tee -a ../evidence/command-output.txt
docker exec club-api-lab9 env | grep -E "NODE_ENV|PORT"
curl -s localhost:3001/healthKết quả mong đợi: uid=1000(node) gid=1000(node); NODE_ENV=production và PORT=3000; /health vẫn trả healthy.
- Ghi đè biến môi trường lúc chạy (không cần build lại):
docker run --rm -e PORT=4000 -d --name club-api-port club-api:lab9
sleep 1
docker logs club-api-port
docker rm -f club-api-portKết quả mong đợi (thêm sleep 1 trước docker logs nếu log còn trống): log in API listening on port 4000. ENV trong Dockerfile là giá trị mặc định, -e ghi đè được khi chạy. Đây là cơ chế để cấu hình DB_HOST, DB_PASSWORD ở buổi sau mà không chép secret vào image.
Phần 4. CMD và ENTRYPOINT (khoảng 10 phút)
- Ghi đè CMD bằng lệnh khác:
docker run --rm club-api:lab9 node --version
docker run --rm club-api:lab9 idKết quả mong đợi: in phiên bản Node 22 (dạng v22.x.x) rồi uid=1000(node).... Lệnh sau tên image thay thế hoàn toàn CMD, rất tiện để debug.
- Tạo biến thể dùng
ENTRYPOINT: chépDockerfilethànhDockerfile.ep(cùng thư mục) và thay dòngCMDcuối bằng:
ENTRYPOINT ["node"]
CMD ["server.js"]- Build và thử:
docker build -f Dockerfile.ep -t club-api:lab9-ep .
docker run --rm club-api:lab9-ep --version
docker run --rm --entrypoint id club-api:lab9-epKết quả mong đợi: lệnh đầu in v22.x.x (tham số --version được nối vào node); lệnh sau in thông tin user nhờ --entrypoint thay thế chương trình chính.
- Kiểm tra việc dừng container có gọn không:
time docker stop club-api-lab9Kết quả mong đợi: lệnh mất khoảng 10 giây (real 0m10...). Lý do: node là PID 1 trong container, server.js không xử lý SIGTERM, kernel không áp dụng hành vi mặc định cho PID 1, nên Docker chờ hết thời gian chờ rồi gửi SIGKILL. Thử lại: docker rm -f club-api-lab9, rồi docker run -d --init --name club-api-lab9 -p 3001:3000 club-api:lab9 và time docker stop club-api-lab9: có tiến trình init nhỏ chuyển tín hiệu nên dừng nhanh. (Cách sửa tận gốc trong mã: bắt SIGTERM và đóng server, như examples/docker/hello-node/server.js đã làm.)
Phần 5. Build context và .dockerignore (khoảng 8 phút)
- Giả lập file bí mật và file lớn nằm nhầm trong thư mục dự án:
cd ~/devops-labs/lab-09/source
echo "DB_PASSWORD=gia-lap-khong-phai-that" > .env
dd if=/dev/zero of=rac.bin bs=1M count=100 status=none
docker build --progress=plain -t club-api:lab9-leak . 2>&1 | grep -i "transferring context"
docker run --rm club-api:lab9-leak ls -a /appKết quả mong đợi: dòng transferring context cho thấy khoảng 100 MB (ví dụ 104.86MB; con số chính xác tùy phiên bản Docker); ls -a /app liệt kê cả .env và rac.bin: file bí mật đã nằm trong image. Ai pull image đều đọc được.
- Tạo
source/.dockerignore:
node_modules
npm-debug.log
.git
.env
*.bin
coverage
tests
Dockerfile*- Build lại và kiểm tra:
docker build --progress=plain -t club-api:lab9-clean . 2>&1 | grep -i "transferring context"
docker run --rm club-api:lab9-clean ls -a /app | tee -a ../evidence/command-output.txt
rm -f .env rac.binKết quả mong đợi: context chỉ còn rất nhỏ (từ vài trăm byte đến vài chục KB, BuildKit chỉ gửi phần thay đổi so với lần trước; điều quan trọng là không còn khoảng 100 MB); ls -a /app không còn .env hay rac.bin. Lưu ý: bỏ tests khỏi image runtime là chủ ý, vì test chạy ở CI (Buổi 13).
Phần 6. Ghi bằng chứng và dọn dẹp (khoảng 5 phút)
cd ~/devops-labs/lab-09
docker image ls | grep club-api | tee -a evidence/command-output.txt
docker rm -f club-api-lab9
docker rmi club-api:lab9-leak club-api:lab9-epDựng lại container cuối cùng để chụp ảnh curl /health vào screenshots/ nếu mentor yêu cầu.
L1 – Foundation: checklist hoàn thành
-
source/Dockerfilebuild thành công, image tênclub-api:lab9. -
curl localhost:3001/healthtrảhealthykhi container chạy. - Dockerfile chép
package.jsonvàpackage-lock.jsontrước khi chạynpm ci. -
docker exec club-api-lab9 idcho thấy usernode, không phải root. - Chạy được
docker run --rm club-api:lab9 node --version. - Có
source/.dockerignoreloại.envvànode_modules; đã chứng minh.envkhông còn trong image.
L2 – Engineering: thử thách tự làm
Thử thách 1. Viết lại không xem mẫu. Xóa Dockerfile, viết lại từ đầu cho đạt đồng thời: image dựa trên Node 22 Alpine, chạy bằng user node, cache dependency đúng, không chứa .env, có HEALTHCHECK gọi /health. Tiêu chí: docker inspect --format '{{.State.Health.Status}}' club-api-lab9 về healthy sau tối đa 1 phút.
Gợi ý
HEALTHCHECK chạy lệnh bên trong container. Image Alpine có wget (BusyBox) nhưng không có curl. Dùng --start-period để cho app thời gian khởi động. Mẫu tham khảo nằm ở examples/docker/hello-node/Dockerfile.optimized.
Lời giải
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production PORT=3000
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/health || exit 1
CMD ["node", "server.js"]Kiểm tra: docker run -d --name club-api-lab9 -p 3001:3000 club-api:lab9 rồi docker ps (cột STATUS có healthy). Dùng 127.0.0.1 thay vì localhost để tránh phân giải ra IPv6 trong một số image.
Thử thách 2. Giải thích CMD và ENTRYPOINT. Trong report.md, viết một đoạn ngắn (5-8 dòng) giải thích: khi nào bạn chọn CMD, khi nào chọn ENTRYPOINT, vì sao dùng exec form; kèm bằng chứng lệnh docker run bạn đã thử ở Phần 4.
Gợi ý
Nghĩ về hai người dùng image: một người muốn chạy API, một người muốn vào debug bằng sh. Với CMD, người thứ hai chỉ cần thêm lệnh sau tên image. Với ENTRYPOINT, họ phải nhớ --entrypoint. Về exec form, hãy so sánh PID 1 và tín hiệu khi docker stop.
Lời giải
Chọn CMD ["node", "server.js"] cho ứng dụng vì mặc định phải chạy API nhưng vẫn dễ ghi đè (docker run image sh) khi debug. Chọn ENTRYPOINT khi image đóng vai một công cụ luôn chạy một chương trình và người dùng chỉ đổi tham số, và có thể kết hợp với CMD làm tham số mặc định. Exec form không qua /bin/sh -c, nên process của ứng dụng nhận trực tiếp tín hiệu từ docker stop; shell form thêm một lớp shell và có thể làm tín hiệu không đến được ứng dụng.
Thử thách 3. Sửa lỗi sai build context (Tình huống 05). Làm phần "Phá lỗi và debug" bên dưới, ghi kết quả điều tra vào report.md. Tiêu chí: nêu được nguyên nhân gốc và hai cách sửa khác nhau.
Thử thách 4. So sánh với Dockerfile của dự án. Đọc starter-project/backend/Dockerfile (multi-stage). Liệt kê ít nhất 3 điểm khác với Dockerfile của bạn và đoán lợi ích của từng điểm (Buổi 10 sẽ đi sâu).
Gợi ý
Đếm số FROM. Xem COPY --from. Xem bước npm ci nằm ở stage nào và image cuối có chứa công cụ build hay không.
Lời giải
Điểm khác chính: (1) hai stage (dependencies và runtime), stage cuối chỉ nhận node_modules đã cài qua COPY --from=dependencies, nên image cuối gọn và không giữ cache npm; (2) dùng npm ci --omit=dev ở stage riêng; (3) user node, NODE_ENV=production, --chown giống bản của bạn. Lợi ích: image nhỏ hơn, bề mặt tấn công nhỏ hơn, cache tốt hơn.
Phá lỗi và debug
Tình huống: build thất bại vì sai build context
Liên quan tới Tình huống 05 trong examples/troubleshooting/scenario-05/ (mentor phát đề cho bạn, chỉ cung cấp triệu chứng).
Cách phá. Đứng ở thư mục cha của source rồi build:
cd ~/devops-labs/lab-09
docker build -t club-api:lab9 .Triệu chứng. Build dừng ngay với thông báo không đọc được Dockerfile, dạng:
ERROR: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directoryNếu bạn chỉ định -f source/Dockerfile nhưng vẫn để context là . ở thư mục cha, Dockerfile được tìm thấy nhưng bước COPY package.json package-lock.json ./ báo lỗi dạng "/package.json": not found.
docker build -f source/Dockerfile -t club-api:lab9 .Hướng điều tra từng bước.
- Đọc kỹ dòng lỗi đầu tiên: đang thiếu Dockerfile hay thiếu file trong
COPY? - Chạy
pwdvàls: bạn đang đứng ở đâu, Dockerfile vàpackage.jsonnằm ở thư mục nào? - Nhớ lại: đối số cuối của
docker buildlà context;COPYtìm file theo context, không theo vị trí Dockerfile. - Thử hai cách: đổi thư mục (
cd source) hoặc giữ vị trí nhưng trỏ rõ-f source/Dockerfilevà contextsource.
docker build -f source/Dockerfile -t club-api:lab9 sourceNguyên nhân gốc. Context không chứa các file mà Dockerfile tham chiếu. Lỗi nằm ở lệnh build, không phải ở nội dung Dockerfile.
Cách phòng tránh. Luôn build từ thư mục chứa Dockerfile hoặc ghi rõ cả -f lẫn context; đưa lệnh build chuẩn vào README; ghi lệnh vào script hoặc CI để không phụ thuộc trí nhớ.
Tình huống bổ sung: mất cache vì COPY . . đặt sớm
Đưa COPY . . lên trước RUN npm ci, sửa một dòng mã nguồn rồi build lại: npm ci chạy lại mỗi lần. Triệu chứng là build chậm không rõ lý do. Nguyên nhân gốc: layer COPY . . đổi nội dung nên mọi layer sau bị vô hiệu. Phòng tránh: chép manifest trước, mã nguồn sau (Phần 2).
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
failed to read dockerfile: open Dockerfile: no such file or directory | Chạy docker build sai thư mục, hoặc file tên khác | cd vào thư mục có Dockerfile hoặc dùng -f đường-dẫn kèm context đúng |
COPY báo "/package.json": not found | package.json không nằm trong build context (hoặc bị .dockerignore loại) | Kiểm tra context (đối số cuối) và nội dung .dockerignore |
npm ci chạy lại mỗi lần build | COPY . . đặt trước npm ci | Chép package.json và package-lock.json trước, cài dependency, rồi chép mã nguồn |
npm ci báo thiếu hoặc lệch package-lock.json | Lock file không có trong context hoặc không khớp package.json | Chạy npm install cục bộ để cập nhật lock file, commit cả hai, đảm bảo không bị ignore |
port is already allocated khi docker run -p 3001:3000 | Port 3001 trên host đang bị container hoặc tiến trình khác dùng | docker ps để tìm, dừng container cũ hoặc đổi cổng host (Tình huống 01) |
Container thoát ngay (Exited (0) hoặc Exited (1)) | CMD sai tên file, hoặc app lỗi khi start | docker logs tên-container, kiểm tra đường dẫn trong CMD và WORKDIR (Tình huống 02) |
EACCES: permission denied khi chạy với USER node | File thuộc root, app ghi vào thư mục không có quyền | Dùng COPY --chown=node:node; ghi dữ liệu vào volume hoặc thư mục node sở hữu |
docker stop luôn mất khoảng 10 giây | App không xử lý SIGTERM khi chạy làm PID 1 | Bắt SIGTERM trong mã, dùng exec form, hoặc docker run --init |
API trả 503 ở /ready | Không kết nối được PostgreSQL | Bình thường ở buổi này; sẽ nối DB ở Buổi 11 và 12 |
Tự kiểm tra
EXPOSE 3000có làm chocurl localhost:3000từ máy host chạy được không? Nếu không thì cần gì?- Vì sao
COPY package.json package-lock.json ./phải đứng trướcCOPY . .? - Khác biệt giữa
ENV PORT=3000trong Dockerfile vàdocker run -e PORT=4000là gì? docker run image shtrong hai trường hợp: Dockerfile dùngCMD ["node","server.js"]và Dockerfile dùngENTRYPOINT ["node"]. Kết quả mỗi trường hợp ra sao?- Vì sao không nên đặt
.envhayDB_PASSWORDthật vàoENVhoặcCOPYvào image? - Tình huống: đồng đội báo "build ở máy mình thì được, ở CI báo COPY không tìm thấy
package.json". Bạn kiểm tra những gì, theo thứ tự? - Tình huống: sau khi thêm
USER node, container thoát vớiEACCESkhi ghi file log vào/app/logs. Nguyên nhân và cách sửa? - Vì sao nên ghim
node:22-alpinethay vìnode:latest?
Đáp án
- Không.
EXPOSEchỉ là tài liệu và metadata. Cần-p 3000:3000(hoặc-p cổng-host:3000) khidocker run. - Để layer
npm cichỉ phụ thuộc manifest. NếuCOPY . .đứng trước, mỗi lần sửa mã nguồn sẽ làm hỏng cache và cài lại toàn bộ dependency. ENVlà giá trị mặc định ghi trong image;-eghi đè giá trị đó cho một container cụ thể lúc chạy, không cần build lại.- Với
CMD: chạysh, vào shell. VớiENTRYPOINT ["node"]: thànhnode sh, node cố đọc file tênshvà báo lỗi không tìm thấy module; muốn vào shell phải--entrypoint sh. - Image có thể bị pull và giải nén bởi bất kỳ ai có quyền, lịch sử layer giữ lại nội dung. Secret phải truyền lúc chạy (biến môi trường, Secret trong Kubernetes) và
.envphải nằm trong.dockerignorevà.gitignore. - (a) Đối số context của lệnh build trong workflow, và
working-directory; (b) có.dockerignoreloạipackage.jsonkhông; (c) file có thực sự được commit không (có thể bị.gitignore); (d) phân biệt hoa thường của tên file (Linux phân biệt). - User
nodekhông có quyền tạo thư mục trong/appdo root sở hữu. Sửa: tạo thư mục và gán quyền khi build (RUN mkdir logs && chown node:node logstrướcUSER node), hoặc tốt hơn là ghi log ra stdout/stderr. - Để build tái lập:
latestđổi theo thời gian, cùng Dockerfile hôm nay và tháng sau có thể ra image khác, gây lỗi khó truy vết.
Nộp bài
Nộp vào lab-09/ trong repo cá nhân, theo templates/lab-template.md:
-
README.mdtrả lời đủ 7 câu: bài lab giải quyết vấn đề gì, kiến thức đã dùng, các bước, kết quả mong đợi, lỗi đã gặp, cách khắc phục, có thể cải thiện gì. -
source/chứaDockerfilecuối cùng,.dockerignorevà mã nguồn backend (không cónode_modules,.env,rac.bin). -
evidence/command-output.txtcó: log build,curl /health,docker exec ... id, kết quả so sánhls -a /apptrước và sau.dockerignore. -
report.mdcó giải thích CMD vs ENTRYPOINT và điều tra lỗi build context (nguyên nhân gốc, hai cách sửa). -
screenshots/có ít nhất một ảnhcurl /healththành công (ảnh không thay thế được log). - Đánh dấu mức đã làm (L1 hoặc L2) ở cuối
README.md. - Đã kiểm tra: không commit secret thật;
.envnằm trong.gitignore.
Recap
- Dockerfile là công thức tái lập, mỗi chỉ thị
COPYvàRUNtạo một layer; image là chồng layer cộng metadata. - Build context là thư mục gửi cho builder;
COPYchỉ thấy trong context, và.dockerignoregiữ file thừa, file bí mật ra khỏi image. - Thứ tự quyết định cache: manifest và
npm citrước, mã nguồn sau. EXPOSEchỉ là tài liệu; truy cập từ host cần-p. Cấu hình lúc chạy dùng-e, không nhúng secret vào image.- Chạy bằng
USER nodevà ghim phiên bản image (node:22-alpine) là thói quen bảo mật và tái lập cơ bản. CMDlà lệnh mặc định dễ ghi đè,ENTRYPOINTlà chương trình cố định; dùng exec form để tín hiệu đến đúng process.- Khi build lỗi, đọc dòng lỗi đầu tiên, kiểm tra thư mục hiện tại và context trước khi nghi ngờ nội dung Dockerfile.