Tuần 1 - Ngày 3: Viết Dockerfile
Mục tiêu học tập
- Hiểu cú pháp và vai trò của từng instruction trong Dockerfile
- Viết Dockerfile cho Node.js và Python app
- Tối ưu layer caching để giảm build time
- Áp dụng multi-stage build để giảm image size
- Nắm các best practices: alpine base, non-root user, .dockerignore, COPY vs ADD
1. Cấu trúc Dockerfile
# Comment
# Instruction ARG (duy nhất dùng được trước FROM)
ARG NODE_VERSION=20
# Base image
FROM node:${NODE_VERSION}-alpine
# Metadata
LABEL maintainer="dev@example.com"
LABEL version="1.0"
# Set working directory trong container
WORKDIR /app
# Copy files từ build context vào container
COPY package*.json ./
# Chạy command khi build
RUN npm ci --omit=dev
COPY . .
# Expose port (documentation only — không thực sự mở port)
EXPOSE 3000
# Environment variable mặc định
ENV NODE_ENV=production
# User non-root
USER node
# Command chạy khi container start
CMD ["node", "server.js"]
2. Các instruction quan trọng
FROM — Base image
# Tag cụ thể (khuyến nghị cho production)
FROM node:20-alpine3.19
# Multi-stage: đặt tên stage
FROM node:20-alpine AS builder
FROM python:3.12-slim AS runtime
# Dùng ARG trước FROM (build-time variable)
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
RUN — Chạy command khi build
# Một lệnh mỗi RUN — tạo nhiều layer, tốn cache
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # layer thứ 3 — KHÔNG xoá được file của layer 1 và 2!
# Gộp thành một RUN — một layer, đúng pattern
RUN apt-get update && \
apt-get install -y --no-install-recommends curl vim && \
rm -rf /var/lib/apt/lists/*
# Dùng heredoc (Docker BuildKit — hỗ trợ từ 2022)
RUN <<EOF
apt-get update
apt-get install -y curl
rm -rf /var/lib/apt/lists/*
EOF
COPY vs ADD
# COPY: đơn giản, chỉ copy file/directory — KHUYẾN NGHỊ
COPY src/ /app/src/
COPY package.json package-lock.json ./
# ADD: có tính năng thêm — ít dùng
# - Tự giải nén .tar.gz
# - Có thể COPY từ URL (KHÔNG khuyến nghị — không cache được)
ADD https://example.com/file.tar.gz /tmp/ # bad practice
ADD local-archive.tar.gz /app/ # OK nếu cần giải nén
# Best practice: luôn dùng COPY trừ khi cần tính năng riêng của ADD
ENV và ARG
# ARG: chỉ có trong build time
ARG BUILD_VERSION=1.0.0
# ENV: có trong cả build time và runtime
ENV APP_VERSION=${BUILD_VERSION}
ENV NODE_ENV=production \
PORT=3000
# Trong container: echo $APP_VERSION
# Override lúc build: docker build --build-arg BUILD_VERSION=2.0.0 .
# Override lúc run: docker run -e NODE_ENV=development my-app
CMD và ENTRYPOINT
# CMD: lệnh mặc định khi container start — có thể override lúc docker run
CMD ["node", "server.js"]
CMD ["python", "-m", "gunicorn", "app:app"]
# ENTRYPOINT: lệnh không thể override (chỉ append thêm args)
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run my-image app.py → python app.py
# docker run my-image other_script.py → python other_script.py
# Shell form vs Exec form
CMD node server.js # shell form: /bin/sh -c "node server.js" — PID 1 là sh
CMD ["node", "server.js"] # exec form: node trực tiếp làm PID 1 — KHUYẾN NGHỊ
# PID 1 quan trọng vì nó nhận SIGTERM khi container stop
USER — Non-root
# Tạo user non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Với Node.js official image — đã có sẵn user "node"
USER node
# Chú ý: COPY/RUN trước USER chạy với root; COPY sau USER chạy với user đó
# Cần đảm bảo file được copy thuộc về đúng user
COPY --chown=node:node . .
USER node
WORKDIR
# Tạo và chuyển đến thư mục làm việc
WORKDIR /app
# Tất cả COPY, RUN, CMD sau đây đều tương đối với /app
COPY . . # copy vào /app/
RUN ls # ls /app/
3. Layer caching — tối ưu build time
Vấn đề: dependency thay đổi liên tục
# BAD: npm install chạy lại mỗi lần code thay đổi
WORKDIR /app
COPY . . # copy tất cả — code thay đổi → cache miss
RUN npm ci # chạy lại mỗi lần!
Fix: copy package.json trước
# GOOD: cache npm install riêng với source code
WORKDIR /app
COPY package.json package-lock.json ./ # chỉ copy manifest
RUN npm ci --omit=dev # chỉ invalidate khi package.json đổi
COPY . . # copy source — cache miss chỉ ảnh hưởng layer này
Nguyên tắc: đặt layer thay đổi ít ở trên (gần FROM), layer thay đổi nhiều ở dưới (gần CMD).
4. Multi-stage build
Mục đích
Giảm image size production bằng cách tách giai đoạn build (có compiler, tools) và giai đoạn runtime (chỉ cần binary/artifacts).
Ví dụ: Node.js app
Kết quả: image runtime không chứa TypeScript compiler, source .ts, devDependencies.
Ví dụ: Go app (binary tĩnh)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# Image runtime cực nhỏ — chỉ chứa binary
FROM scratch AS runtime
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# Image size: ~10 MB thay vì ~800 MB
Ví dụ: Python app
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN adduser --disabled-password --gecos '' appuser
USER appuser
EXPOSE 8000
CMD ["python", "-m", "gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
5. .dockerignore
Giống .gitignore — loại trừ file khi gửi build context lên daemon. Ảnh hưởng đến tốc độ build và bảo mật.
# .dockerignore
.git
.gitignore
node_modules # QUAN TRỌNG: tránh copy node_modules từ host vào image
dist
build
*.log
.env # KHÔNG bao giờ bake secrets vào image
.env.*
__pycache__
*.pyc
.pytest_cache
.coverage
Dockerfile*
docker-compose*.yml
README.md
Security note: Thiếu
.dockerignorecó thể vô tình đưa.env, private keys, hoặc.githistory vào image — sau đó push lên ECR/Docker Hub là lộ secrets.
6. Best practices tổng hợp
Chọn base image
# TRÁNH: image quá lớn, nhiều CVE
FROM ubuntu:latest
FROM python:3.12
# TỐT HƠN: slim hoặc alpine — nhỏ hơn, ít CVE
FROM python:3.12-slim # Debian minimal (~50 MB thay vì ~900 MB)
FROM node:20-alpine # Alpine Linux (~5 MB base, ~130 MB với Node)
FROM amazoncorretto:21-alpine # AWS Corretto JDK — optimized cho EC2/Fargate
# SLIM nhất: distroless (Google) — chỉ app + runtime, không shell
FROM gcr.io/distroless/python3
So sánh image size cho Python
| Base image | Size |
|---|---|
python:3.12 | ~1 GB |
python:3.12-slim | ~130 MB |
python:3.12-alpine | ~55 MB |
gcr.io/distroless/python3 | ~50 MB |
Checklist Dockerfile production
[ ] Dùng tag cụ thể cho base image (không dùng :latest)
[ ] .dockerignore đã có, loại trừ .env và node_modules
[ ] Package manifest (package.json / requirements.txt) COPY trước source code
[ ] Multi-stage build nếu có bước compile/build
[ ] Chạy với USER non-root
[ ] Không bake secrets (passwords, API keys) vào image
[ ] EXPOSE document port đúng
[ ] CMD dùng exec form ["node", "server.js"] không phải shell form
[ ] RUN apt/apk cleanup trong cùng layer với install
Câu hỏi ôn tập
-
Tại sao
COPY package.json ./ && RUN npm ciphải đặt TRƯỚCCOPY . .để tận dụng cache?Xem đáp án
Docker cache theo layer: nếu một layer thay đổi, tất cả layer phía dưới bị invalidate. Source code thay đổi thường xuyên, nhưng
package.jsonthay đổi ít hơn. Bằng cách copypackage.jsontrước và chạynpm cithành một layer riêng, layerRUN npm cichỉ bị rebuild khipackage.jsonthay đổi — còn nếu chỉ sửa code thì layer npm install vẫn được cache, tiết kiệm nhiều phút build time. -
Sự khác biệt giữa
CMDshell form và exec form liên quan đến PID 1 là gì?Xem đáp án
Shell form
CMD node server.jschạy qua/bin/sh -c, nên PID 1 làsh, không phảinode. Khi Docker gửi SIGTERM (khidocker stop),shnhận signal nhưng không forward xuốngnode→ process không graceful shutdown.Exec form
CMD ["node", "server.js"]chạynodetrực tiếp làm PID 1, nhận SIGTERM đúng cách. Luôn dùng exec form cho production. -
Multi-stage build giúp gì cho image production?
Xem đáp án
Multi-stage build tách giai đoạn build (có compiler, devDependencies, build tools) khỏi giai đoạn runtime (chỉ artifacts cần thiết). Image cuối không chứa TypeScript compiler, test files, source
.ts, hay devDependencies — giảm size đáng kể (ví dụ từ ~1 GB xuống ~90 MB cho Node.js app) và thu hẹp attack surface (ít package = ít CVE). -
Tại sao
.envPHẢI có trong.dockerignore?Xem đáp án
.envthường chứa secrets (database passwords, API keys, tokens). Nếu không có trong.dockerignore, file sẽ được copy vào build context và có thể bị bake vào image layers. Ai có image đó (pull từ registry) có thể extract secrets quadocker historyhoặcdocker inspect..dockerignorengăn file này vào build context ngay từ đầu — đây là nguyên tắc bảo mật không thể bỏ qua. -
Khi nào dùng
ADDthay vìCOPY?Xem đáp án
Chỉ dùng
ADDkhi cần hai tính năng đặc biệt của nó: (1) tự động giải nén file.tar.gzvào destination directory, hoặc (2) copy từ URL (không khuyến nghị vì không cache được và có security risk). Trong tất cả trường hợp khác — copy file/directory thông thường — dùngCOPYvì rõ ràng và predictable hơn.
Bài tập thực hành
# 1. Tạo ứng dụng Node.js đơn giản
mkdir myapp && cd myapp
cat > package.json << 'EOF'
{
"name": "myapp",
"version": "1.0.0",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^4.18.0" }
}
EOF
cat > server.js << 'EOF'
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello Docker!'));
app.listen(3000, () => console.log('Server running on port 3000'));
EOF
# 2. Tạo .dockerignore
cat > .dockerignore << 'EOF'
node_modules
*.log
.env
EOF
# 3. Tạo Dockerfile
cat > Dockerfile << 'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
EOF
# 4. Build và chạy
docker build -t myapp:1.0 .
docker run -d -p 3000:3000 --name myapp myapp:1.0
curl http://localhost:3000
# 5. Xem layers
docker history myapp:1.0
# 6. Thêm multi-stage build
cat > Dockerfile.multistage << 'EOF'
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]
EOF
docker build -f Dockerfile.multistage -t myapp:multistage .
docker images myapp # so sánh size
# Dọn dẹp
docker rm -f myapp
docker rmi myapp:1.0 myapp:multistage
Tài liệu tham khảo chính thức
Tiếp theo: Ngày 4 — Docker Compose