---
id: docker-dockerfile
title: "Dockerfile"
track: docker
trackTitle: "Docker"
order: 3
level: core
status: published
estimatedMinutes: 45
prerequisites:
  - docker-cli
next: docker-buildkit
reviewedAgainst: 2026-10-19
sources:
  - title: "KodeKloud — Docker Training Course for the Absolute Beginner"
    url: https://learn.kodekloud.com/learn/courses/docker-training-course-for-the-absolute-beginner
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Dockerfile reference"
    url: https://docs.docker.com/reference/dockerfile/
  - title: "Building best practices"
    url: https://docs.docker.com/build/building/best-practices/
  - title: "npm ci"
    url: https://docs.npmjs.com/cli/commands/npm-ci
---

# Dockerfile

Image Node trong lab dùng major `24`, Active LTS theo lịch [nodejs/Release](https://github.com/nodejs/release) tính tới 2026-10-12. Lịch này chuyển `24` sang Maintenance LTS vào 2026-10-20.

## Để làm gì

Viết một Dockerfile lặp lại được, để Docker build ra image có app và runtime. Đồng nghiệp, máy CI, hay Kubernetes dùng đúng image đó, khỏi cài Node bằng tay trên từng máy.

## Gồm những gì

- Image lab chỉ có `server.js`, đủ để build và chạy.
- `COPY` file từ context. `ADD` thêm giải nén tar và URL — best practices khuyên `COPY` trừ khi cần đúng hai việc đó.
- App Python: copy `requirements.txt` trước, cài, rồi mới copy source. Cùng luật cache layer với Node.
- Multi-stage build cho app có `npm ci` và `npm run build`.
- User không phải root trong image cuối.
- `.dockerignore`, kể cả `.env`.
- `docker init` trên Docker Desktop, khi project chưa có bộ file này.
- `docker commit` không thay file này.

`CMD` và `ENTRYPOINT`, cache mount, secret lúc build, và file `.env` nằm ở ba bài kế. Compose dùng image vừa build sau các bài đó. Bài này chưa push image lên registry.

## Dùng khi nào

App cần chạy ngoài laptop đang dev. Mọi dependency phải có trong Dockerfile. Image không dựa vào thứ tình cờ đã cài trên laptop.

Image production bỏ compiler, source map nội bộ, và dependency chỉ dùng lúc test, nếu process chạy không cần chúng.

## Kiến thức cốt lõi

Mỗi chỉ thị trong Dockerfile tạo một layer. Docker dùng lại layer khi lệnh và file được copy không đổi. Copy `package.json` cùng lockfile trước, chạy `npm ci`, rồi mới copy source. Sửa một dòng code mà build lại chạy `npm ci` từ đầu thì thường do thứ tự layer: `COPY` source đang đứng trước lệnh cài dependency.

`npm ci` cài đúng cây dependency trong lockfile và xóa `node_modules` hiện có trước khi cài. Build image thì dùng lệnh này. `npm install` có thể sửa lockfile và ra cây khác máy dev.

`npm ci --omit=dev` bỏ `devDependencies`. Dùng ở stage chạy production, sau khi stage build đã biên dịch xong.

Multi-stage build có nhiều `FROM`. Image cuối chỉ `COPY --from` phần process cần chạy. Toolchain của stage build không đi vào image cuối.

Image `node` chính thức đã có user `node`. `USER node` cho process chạy bằng user đó. User này phải đọc được file vừa `COPY`. Quyền mặc định sau `COPY` đủ để user đó đọc file.

`EXPOSE` là metadata: image định dùng port nào. Port đó chưa mở ra host cho đến khi `docker run --publish`.

`COPY` copy file từ build context, hoặc từ stage khác (`COPY --from`). `ADD` thêm hai việc: lấy file từ URL HTTPS/Git, và tự giải nén tar khi nguồn nằm trong context. Best practices: file thường thì `COPY`; `ADD` khi thật sự cần artifact remote. Lab "sửa Dockerfile cho build được" hay lẫn path `COPY`, `CMD` dạng chuỗi, hoặc `ADD` URL không cần — đọc từng chỉ thị, đừng đoán.

App Python cùng một thứ tự layer: copy file khóa dependency trước (`requirements.txt`), `RUN` lệnh cài, rồi mới `COPY` source. Image gốc pin tag của [python trên Docker Hub](https://hub.docker.com/_/python), không `python:latest`. `docker commit` một container đã `apt install` không cho ra Dockerfile lặp lại được.

## Lệnh và code mẫu

Lab tối thiểu, để trong thư mục trống `$HOME/devops-lab/docker/app`.

`server.js`:

```javascript
const http = require("node:http");

const port = Number(process.env.PORT || 3000);
const server = http.createServer((req, res) => {
  const body = req.url === "/healthz" ? "ok" : "hello from container";
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
  res.end(body);
});

server.listen(port, "0.0.0.0");
```

`Dockerfile`:

```dockerfile
# syntax=docker/dockerfile:1

FROM node:24-alpine
WORKDIR /app
COPY server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]
```

`CMD` dạng exec, mảng JSON, chạy `node` trực tiếp. Dạng chuỗi đi qua shell, nên signal không tới `node` trực tiếp như dạng exec. Argument viết sau tên image lúc `docker run` thay cả dòng `CMD` này. Khi nào argument chỉ được nối thêm, bài CMD và ENTRYPOINT nói tiếp.

Build và chạy:

```bash
cd "$HOME/devops-lab/docker/app"
docker build -t devops-lab-node:1 .
docker run --name lab-node --detach --rm --publish 3000:3000 devops-lab-node:1
curl -sS http://127.0.0.1:3000/healthz
docker stop lab-node
```

App có lockfile và bước build thì tách stage:

```dockerfile
# syntax=docker/dockerfile:1

FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:24-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:24-alpine AS prod-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=prod-deps /app/node_modules ./node_modules
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
```

Mẫu này viết cho app có `npm run build` ghi ra `dist/` và lệnh chạy là `node dist/main.js`. App thật thì đổi hai chỗ đó.

`.dockerignore` nên có ít nhất:

```text
node_modules
npm-debug.log
.git
dist
.env
```

Quên `node_modules` trong `.dockerignore` thì `COPY . .` mang cây dependency của máy dev vào, `npm ci` trong image mất tác dụng. Quên `.env` thì file mật khẩu trên máy dev đi vào context. Bài biến môi trường nói file đó ở trên host, đọc lúc chạy container.

Project chưa có Dockerfile thì Docker Desktop có `docker init`. Lệnh hỏi nền tảng (Node, Python, Go, …), version và port, rồi tạo `.dockerignore`, `Dockerfile`, `compose.yaml` và `README.Docker.md`. File nào đã có, lệnh hỏi trước khi ghi đè. Bản bị đè không lấy lại được: đổi tên file cũ trước khi đồng ý. Engine trên Linux server không kèm lệnh này. Đó là plugin của Docker Desktop. File sinh ra là điểm bắt đầu. Đọc lại tag image, `USER` và `CMD` trước khi chạy `docker compose up --build`.

Python, cùng luật cache. Pin tag image, đừng `latest`:

```dockerfile
# syntax=docker/dockerfile:1

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
```

`pip install -r` là dạng docs pip ghi cho requirements file. Tên file app (`app.py`) đổi theo project. `requirements.txt` phải có trong context trước `COPY`. Tag `3.12-slim` là ví dụ pin; kiểm tra tag còn trên [Docker Hub — python](https://hub.docker.com/_/python) lúc build.

## Thực hành

1. Tạo `server.js` và `Dockerfile` như lab tối thiểu.
2. Build với tag `devops-lab-node:1`.
3. Chạy container và gọi `/healthz`.
4. Sửa chuỗi trả về rồi build lại. Trước lần build đó, tag `devops-lab-node:1` vẫn trỏ image cũ.
5. Chạy `docker image ls devops-lab-node`.

## Kết quả mong đợi

- `curl` in `ok`.
- `docker top lab-node` cho thấy process chính là `node server.js`. Image Alpine không cần cài thêm `ps`.
- Image có tag `devops-lab-node:1`.
- App nghe `0.0.0.0:3000` trong container. Máy host vào qua port đã publish.

## Checklist

- [ ] Giải thích vì sao copy lockfile trước source ở stage có `npm ci`.
- [ ] Chọn `npm ci` cho build lặp lại.
- [ ] Đặt `USER` khác root khi image gốc đã có user phù hợp.
- [ ] Nhớ `EXPOSE` là metadata, publish port bằng `--publish`.
- [ ] File thường: `COPY`. `ADD` chỉ khi cần giải nén tar hoặc URL.
- [ ] Python: copy `requirements.txt` trước, cài, rồi mới copy source. Pin tag, không `latest`.
- [ ] Khai `.env` trong `.dockerignore`. Trên Docker Desktop, `docker init` chỉ là bản nháp: đọc lại tag, `USER` và `CMD` trước khi up.

## Thẻ ôn

### docker-dockerfile-layer
kind: decide
cue: Sửa một dòng code mà `npm ci` chạy lại từ đầu: đang copy source trước lúc cài dependency. Copy lockfile trước, cài xong rồi mới copy source.

Q: Bạn sửa một dòng trong source rồi `docker build`. Log cho thấy `npm ci` chạy lại từ đầu, dù `package-lock.json` không đổi. Thứ tự layer trong Dockerfile đang sai chỗ nào, và viết lại thế nào để lần sau chỉ build phần code?

A: Docker dùng lại layer khi lệnh và file đầu vào của layer đó không đổi. Nếu `COPY` cả source đứng trước lệnh cài dependency, mỗi lần sửa code là layer copy đổi, và mọi lệnh phía sau, kể cả `npm ci`, phải chạy lại. Copy `package.json` cùng lockfile trước, chạy `npm ci`, rồi mới copy source. Khi lockfile không đổi, layer cài dependency được dùng lại. Chỉ layer copy source và các lệnh sau nó là build mới.

### docker-dockerfile-ci
kind: command
cue: Trong image, cài dependency bằng `npm ci` để đúng cây trong lockfile. Stage chạy production thêm `--omit=dev`.

Q: Dockerfile đang dùng `npm install` để cài dependency lúc build. Vì sao image CI và máy dev có thể ra hai cây package khác nhau, và lệnh nên dùng là gì?

A: `npm install` được phép cập nhật lockfile và chọn bản nằm trong khoảng version. Hai lần cài có thể không giống nhau. `npm ci` làm ngược lại: nó đòi lockfile, xóa `node_modules` hiện có, rồi cài đúng cây đã khóa. Build image thì dùng `npm ci`. Stage nào chỉ để chạy production thì `npm ci --omit=dev`, để devDependency không đi theo image chạy thật.

### docker-dockerfile-expose
kind: distinguish
cue: `EXPOSE 3000` chỉ ghi image định dùng port nào. Port chưa mở ra máy host cho đến khi `docker run --publish`.

Q: Dockerfile có dòng `EXPOSE 3000`. Bạn build xong, chạy container, mở `http://127.0.0.1:3000` trên máy mình và không vào được. `EXPOSE` đã làm gì, và còn thiếu lệnh nào?

A: `EXPOSE` là metadata. Nó nói với người đọc image, và với một số tool, rằng process trong image định nghe port 3000. Nó không mở port đó trên máy host, cũng không publish ra mạng. Muốn trình duyệt trên host vào được, lúc chạy phải `docker run --publish`, ví dụ `3000:3000`. Không có `--publish` thì port chỉ tồn tại bên trong container.

### docker-dockerfile-cmd
kind: command
cue: `CMD node server.js` đi qua shell, signal không tới `node`. Viết dạng exec `CMD ["node", "server.js"]` để `node` là process chính.

Q: Một Dockerfile ghi `CMD node server.js`. `docker stop` gửi SIGTERM nhưng process Node không nhận signal để đóng kết nối. Dạng chuỗi khác dạng `CMD ["node", "server.js"]` ở chỗ nào?

A: Dạng chuỗi được Docker đưa qua shell, thường là `sh -c`. Process chính lúc đó là shell, không phải `node`, nên signal dừng container tới shell trước và có thể không chuyển tới Node. Dạng exec là một mảng JSON: Docker chạy đúng `node` làm PID 1, signal đi thẳng tới process đó. Viết `CMD ["node", "server.js"]`.

### docker-dockerfile-ignore
kind: trap
cue: `COPY . .` mà thiếu `.dockerignore` sẽ mang `node_modules` của máy dev vào image, và `npm ci` không còn đáng tin.

Q: Dockerfile có `COPY . .` rồi `npm ci`. Máy dev đã có sẵn `node_modules`, và `.dockerignore` không khai báo thư mục đó. Vì sao cây dependency trong image không còn đáng tin?

A: Build context là những gì Docker gửi cho daemon trước khi chạy Dockerfile. `COPY . .` chép cả context, kể cả `node_modules` của máy dev, vào image. `npm ci` lẽ ra xóa và cài lại theo lockfile, nhưng cây vừa copy vào có thể đè hoặc lẫn với bản cài, và image mang theo dependency của một máy cụ thể. Khai báo `node_modules`, `.git` và `dist` trong `.dockerignore` để chúng không vào context.

### docker-dockerfile-copy-add
kind: distinguish
cue: File từ context: `COPY`. `ADD` dành cho URL HTTPS/Git hoặc tự giải nén tar. Best practices: phần lớn trường hợp dùng `COPY`.

Q: Dockerfile cần đưa `requirements.txt` và source vào image. Dùng `ADD` hay `COPY`, `ADD` còn làm thêm gì, và vì sao lab hay fail ở đúng hai chỉ thị này?

A: File nằm trong build context thì `COPY`. `ADD` cũng copy được, nhưng còn lấy URL HTTPS/Git và tự giải nén tar local. Best practices: `COPY` cho file thường; `ADD` khi thật sự cần artifact remote. Lab sửa Dockerfile hay sai path nguồn (`COPY` file không có trong context), hoặc `ADD` một URL không cần. Đọc lỗi build: "not found" là path/`COPY`. Muốn cache pip/npm thì copy file dependency trước source, không đổi sang `ADD` cho "cho chắc".

## Lỗi thường gặp

**`npm install --only=production`.** `--only` là cú pháp cũ. npm hiện tại dùng `--omit=dev`. Trong image, cài từ lockfile bằng `npm ci --omit=dev`.

**`CMD node server.js` dạng chuỗi.** Signal không tới process trực tiếp như dạng exec. Viết `CMD ["node", "server.js"]`.

**Copy cả Git history và `node_modules` vào build context.** Build chậm, file không định đưa vẫn lọt vào image. Khai báo chúng trong `.dockerignore`.

**`ADD https://…` cho một file đã có trong context.** Best practices để `COPY` cho file local. `ADD` khi cần tải remote.

**`docker commit` rồi coi đó là Dockerfile của app Python.** Volume không vào image; lệnh cài không đọc lại được. Viết `FROM python:…`, copy `requirements.txt`, `pip install -r`, rồi copy source.

## Nguồn

1. [KodeKloud — Docker for the Absolute Beginner](https://learn.kodekloud.com/learn/courses/docker-training-course-for-the-absolute-beginner) — module Docker Images: viết Dockerfile và `docker init`. Bài viết lại, không chép lab.
2. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày Write a Docker File, Resolve Dockerfile Issues, Docker Python App. Trang practice cần đăng nhập.
3. [Dockerfile reference](https://docs.docker.com/reference/dockerfile/) — `FROM`, `COPY`, `ADD`, `USER`, `CMD`, `EXPOSE`.
4. [docker init](https://docs.docker.com/reference/cli/docker/init/) — file Docker Desktop tạo, và cảnh báo ghi đè.
5. [Building best practices](https://docs.docker.com/build/building/best-practices/) — `COPY` với file thường, `ADD` khi cần remote/tar, thứ tự layer, multi-stage, `.dockerignore`.
6. [pip install](https://pip.pypa.io/en/stable/cli/pip_install/) — `-r` / `--requirement`.
7. [npm ci](https://docs.npmjs.com/cli/commands/npm-ci) — cài từ lockfile.
8. [Node.js release schedule](https://github.com/nodejs/release) — chọn major còn hỗ trợ.
9. [Docker Hub — python](https://hub.docker.com/_/python) — tag image official.

## Tiếp theo

[BuildKit](./08-buildkit.md)
