Kubernetes cho Frontend Developer · Phần 4 — Dockerfile chuẩn cho frontend, registry và Docker Compose
Dockerfile multi-stage cho React/Vue/Vite ra image khoảng 25 MB, nginx.conf cho SPA với try_files, .dockerignore, quy ước tên và tag image, push lên ghcr.io, imagePullSecrets, và Docker Compose như bước đệm trước K8s.
Bạn đã chạy được container nginx ở Phần 3. Giờ là lúc đóng gói chính app của bạn: viết một Dockerfile mà build ra image khoảng 25 MB thay vì 1.2 GB, kèm nginx.conf để F5 ở /about không ra 404, đẩy image đó lên registry để cluster kéo về được, và chạy thử cả frontend + API + database bằng Docker Compose để làm quen với ý tưởng “gọi nhau bằng tên” trước khi gặp Service của K8s.
Đây là bài cuối của chặng nền tảng. Kết thúc bài, bạn có một image thật trên ghcr.io với tag bất biến, và mọi phần K8s từ phần 5 trở đi sẽ deploy chính image đó. Nếu bạn muốn một chặng Docker chậm hơn, series Docker for Developers trên blog đi kỹ từng chỉ thị Dockerfile; bài này tập trung vào đúng thứ frontend cần.
Viết Dockerfile chuẩn cho app frontend
Tư duy: hai giai đoạn khác nhau
App frontend có hai pha hoàn toàn khác nhau:
- Pha build: cần Node.js, npm, toàn bộ
node_modules(có thể 500 MB trở lên), TypeScript, Vite, ESLint. Chỉ dùng một lần. - Pha chạy: chỉ cần một web server phục vụ file tĩnh trong
dist/(khoảng 2 MB). Không cần Node, không cầnnode_modules.
Nếu gộp cả hai vào một image, bạn sẽ có image 1.2 GB chứa đầy thứ không cần thiết, và mỗi thứ thừa là thêm thời gian pull, thêm lỗ hổng bảo mật, thêm bề mặt tấn công. Giải pháp là multi-stage build: stage 1 build bằng Node, stage 2 chỉ copy dist/ sang một image nginx nhỏ. Mọi thứ của stage 1 bị bỏ lại.
Với Next.js SSR thì khác: pha chạy vẫn cần Node, nhưng vẫn multi-stage để bỏ devDependencies và source. Phần 11 có Dockerfile riêng cho trường hợp đó; bài này lo SPA.
Dockerfile hoàn chỉnh cho React / Vue / Vite SPA
# ---------- STAGE 1: BUILD ----------
FROM node:20-alpine AS builder
WORKDIR /app
# Copy manifest TRƯỚC để tận dụng cache layer
COPY package*.json ./
# npm ci nhanh và tất định hơn npm install (dùng đúng package-lock.json)
RUN npm ci
# Copy source code SAU
COPY . .
RUN npm run build
# Kết quả nằm ở /app/dist (Vite) hoặc /app/build (CRA)
# ---------- STAGE 2: RUNTIME ----------
FROM nginx:1.27-alpine
# Chỉ lấy thư mục dist từ stage builder. node_modules bị bỏ lại hoàn toàn.
COPY --from=builder /app/dist /usr/share/nginx/html
# Cấu hình nginx riêng cho SPA
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Đọc từng khối:
FROM node:20-alpine AS builderđặt tên stage để stage sau tham chiếu bằng--from=builder. Chọnalpinevì nhỏ; nếu app cần native module (sharp,node-gyp) mà alpine build lỗi, đổi sangnode:20-slim(Debian, lớn hơn chút nhưng tương thích hơn).COPY package*.json ./copypackage.jsonvàpackage-lock.json. Nếu dùng pnpm thì copypnpm-lock.yamlvà chạycorepack enable && pnpm install --frozen-lockfile; yarn thìyarn.lockvàyarn install --frozen-lockfile. Ý tưởng không đổi: lockfile trước, source sau.RUN npm cicài đúng phiên bản trong lockfile, xoánode_modulescũ nếu có, và fail nếu lockfile lệch vớipackage.json. Đó là hành vi bạn muốn trong build: tất định.COPY --from=builder /app/dist /usr/share/nginx/htmllà toàn bộ điểm của multi-stage. Đường dẫn/usr/share/nginx/htmllà thư mục web root mặc định của image nginx, bạn đãlsnó ở phần 3. CRA xuất rabuild/thay vìdist/, sửa đường dẫn cho khớp.EXPOSE 80chỉ là ghi chú cho người đọc và công cụ; nó không tự mở cổng, bạn vẫn cần-pkhidocker run.
Ghi chú: vì sao
daemon off;? Container sống chừng nào tiến trình chính (PID 1) còn chạy. nginx mặc định tự đẩy mình vào chạy nền (daemon) rồi tiến trình gốc thoát; PID 1 kết thúc và container tắt ngay với trạng tháiCompleted.-g "daemon off;"bắt nginx chạy foreground. Nguyên tắc chung cho mọi container: tiến trình chính phải chạy foreground. Ở K8s, Pod chạy xong rồiCompletedvới một web server là dấu hiệu chắc chắn của lỗi này.
Dạng CMD ["nginx", "-g", "daemon off;"] (mảng JSON, gọi là exec form) khác dạng CMD nginx -g "daemon off;" (shell form): exec form chạy nginx trực tiếp làm PID 1, shell form bọc nó trong sh -c và sh trở thành PID 1, nuốt mất SIGTERM mà K8s gửi khi tắt Pod (phần 2 đã nói về tín hiệu). Luôn dùng exec form.
File nginx.conf cho SPA
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Bật nén - giảm 70% dung lượng truyền tải
gzip on;
gzip_types text/plain text/css application/json application/javascript
application/xml text/javascript image/svg+xml;
gzip_min_length 1024;
# Asset có hash trong tên -> cache vĩnh viễn
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# QUAN TRỌNG NHẤT: fallback về index.html cho client-side routing
location / {
try_files $uri $uri/ /index.html;
}
# index.html TUYỆT ĐỐI không được cache
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Endpoint cho K8s health check
location /healthz {
access_log off;
return 200 "ok\n";
}
}
Đọc từng khối theo thứ tự quan trọng:
try_files $uri $uri/ /index.html; là lời giải cho lỗi 404-khi-F5. Dòng này nói với nginx: “tìm file thật trước ($uri), rồi thư mục ($uri/); không có thì trả index.html để React Router tự xử lý đường dẫn”. Thiếu dòng này, mọi deploy SPA đều lỗi khi người dùng F5 hoặc mở link trực tiếp, vì /users/123 không phải file nào trong dist/. Bấm link nội bộ vẫn chạy vì đó là History API, không có request nào ra server; chỉ khi request thật tới nginx thì mới lộ.
location /assets/ với immutable. Vite (và Webpack) đặt hash nội dung vào tên file: main-a1b2c3.js. Đổi nội dung là đổi tên, nên file cũ không bao giờ bị ghi đè và trình duyệt có thể cache một năm không cần hỏi lại. immutable bảo trình duyệt đừng revalidate ngay cả khi người dùng F5.
location = /index.html với no-cache, no-store. index.html là điểm vào, không có hash trong tên, phải luôn mới để trỏ tới bundle mới. Đây là nguyên nhân số một của “deploy xong user vẫn thấy bản cũ” và “trang trắng sau deploy”: index.html cũ bị cache, trỏ tới main-abc.js đã bị xoá. Vì try_files fallback thực hiện một internal redirect tới /index.html, khối location = /index.html cũng áp dụng cho mọi route SPA, nên /users/123 cũng không bị cache. Phần 12 mở rộng thành chiến lược cache đầy đủ.
gzip giảm khoảng 70% dung lượng JS/CSS truyền đi. gzip_min_length 1024 bỏ qua file quá nhỏ (nén không đáng). Nếu có CDN đứng trước cluster, CDN thường nén lại; bật ở nginx vẫn không hại.
/healthz trả 200 ok không đụng vào filesystem, dùng cho readiness và liveness probe ở phần 8. access_log off để probe gọi mỗi 5 giây không làm rác log.
Hai chi tiết nginx dễ vấp: add_header trong một location thay thế hoàn toàn các add_header ở tầng server, không kế thừa cộng dồn; nếu sau này bạn thêm security header ở tầng server, nhớ lặp lại chúng trong các location có add_header riêng. Và luôn kiểm tra cú pháp bằng docker run --rm -v "$PWD/nginx.conf:/etc/nginx/conf.d/default.conf" nginx:1.27-alpine nginx -t trước khi build, thay vì build xong mới thấy container chết. Bài Nginx configuration model trên blog giải thích cách nginx chọn location và kế thừa directive.
File .dockerignore: đừng quên
node_modules
dist
build
.git
.env
.env.local
*.log
coverage
.vscode
README.md
Dockerfile
.dockerignore
Cú pháp giống .gitignore. Docker gửi toàn bộ thư mục hiện tại (build context) lên daemon trước khi build; .dockerignore loại những gì không cần.
Cạm bẫy: không có
.dockerignore, lệnhCOPY . .sẽ copy cảnode_moduleslocal (có thể build cho macOS ARM) đè lênnode_modulesvừa cài trong container (build cho Linux) → lỗi native module rất khó hiểu, kiểuError: Cannot find module '../build/Release/sharp-linux-x64.node'. Ngoài ra nó làm build chậm gấp nhiều lần (gửi 500 MB context) và có nguy cơ đưa file.envchứa secret vào image, nơi bất kỳ aidocker pullđược đều đọc được bằngdocker run image cat .env.
Ghi dist vào .dockerignore tưởng ngược đời (đó là thứ ta cần), nhưng dist được build lại bên trong stage 1, còn dist trên máy bạn có thể cũ hoặc build với biến môi trường khác. Mọi thứ trong image phải sinh ra từ Dockerfile, không phải từ trạng thái máy bạn.
Kiểm chứng kết quả tối ưu
| Cách làm | Dung lượng image |
|---|---|
Một stage, FROM node:20, giữ node_modules | ~1.2 GB |
Một stage, node:20-alpine | ~450 MB |
Multi-stage + nginx:alpine | ~25 MB |
Image nhỏ hơn 40 lần nghĩa là: pull nhanh hơn, Pod khởi động nhanh hơn, scale nhanh hơn, tốn ít băng thông registry và ít lỗ hổng bảo mật hơn (ít package hệ thống hơn để bị CVE). Kiểm tra bằng docker images my-app; muốn xem layer nào chiếm bao nhiêu, cài dive (dive my-app:v1).
Một lưu ý về kiến trúc CPU: Mac M-series build ra image ARM64. Cluster cloud thường chạy x86-64, kéo image ARM về sẽ lỗi exec format error và Pod CrashLoopBackOff không có log gì đáng đọc. Build đúng nền tảng bằng docker build --platform linux/amd64 -t my-app:v1 ., hoặc để CI (chạy trên x86) build thay bạn như phần 16. Bài BuildKit, multi-platform và supply chain đi sâu hơn.
Thực hành 1.5
- Tạo app mới:
npm create vite@latest my-app -- --template react-ts, rồicd my-app && npm install. - Thêm React Router (
npm install react-router-dom) với hai route/và/about, có link qua lại. Chạynpm run dev, xác nhận bấm link chuyển trang. - Viết
Dockerfile,nginx.conf,.dockerignorenhư trên, đặt ở thư mục gốc app. docker build -t my-app:v1 .rồidocker images | grep my-app. Kiểm tra dung lượng: phải dưới 50 MB.docker run -d -p 8080:80 --name my-app my-app:v1, mởlocalhost:8080/aboutrồi F5. Phải chạy được. Chạycurl -I http://localhost:8080/assets/với tên file JS thật (xem trongdocker exec my-app ls /usr/share/nginx/html/assets) và đọc headerCache-Control.- Thử xoá dòng
try_files, build lại thànhmy-app:v1-broken, chạy ở cổng 8081, mở/aboutrồi F5 → thấy 404 của nginx. Rồi khôi phục. Đây là bài học đáng giá nhất của chặng nền tảng. - Dọn dẹp:
docker rm -f my-appvà container broken.
Kết quả mong đợi:
docker imagescho thấymy-app:v1khoảng 25–45 MB.curl -I http://localhost:8080/healthztrả200.curl -I http://localhost:8080/abouttrả200(không phải 404) và cóCache-Control: no-cache, no-store, must-revalidate. Với bản broken,/abouttrả404.
Image Registry: nơi cất giữ image
Image build ở máy bạn nằm trên ổ cứng máy bạn. Cluster K8s (ở nơi khác) không thấy được. Bạn cần đẩy image lên registry, hệ thống lưu trữ image, hệt như npm registry lưu package. docker push tương đương npm publish, docker pull tương đương npm install.
Quy ước đặt tên image
ghcr.io / cong-ty / my-frontend : v1.2.0
| | | |
registry owner tên image tag
Ví dụ thực tế:
docker.io/library/nginx:1.27-alpine # Docker Hub (mặc định)
ghcr.io/my-org/web-app:v2.1.0 # GitHub Container Registry
123456.dkr.ecr.ap-southeast-1.amazonaws.com/web:sha-a1b2c3 # AWS ECR
Khi bạn viết nginx:1.27-alpine không có registry, Docker hiểu ngầm là docker.io/library/nginx:1.27-alpine. Docker Hub giới hạn số lần pull ẩn danh trong một khoảng thời gian; cluster nhiều node pull cùng lúc có thể vướng giới hạn này với lỗi toomanyrequests. Với image của chính bạn, dùng ghcr.io (miễn phí cho repo public, đi kèm GitHub) hoặc registry của cloud đang dùng (ECR, GCR/Artifact Registry) để gần cluster.
Quy tắc đặt tag: cực kỳ quan trọng
Nguy hiểm: KHÔNG BAO GIỜ dùng tag
:latesttrên production.latestchỉ là một cái tên, nó trỏ tới image nào thì không ai chắc. Khi Pod restart, nó có thể kéo về một image khác với image đang chạy → hai Pod cùng Deployment chạy hai version khác nhau. Và bạn không thể rollback vì không biết version cũ là gì. Hãy luôn dùng tag bất biến:v1.2.3hoặcsha-a1b2c3d(commit hash).
Ba lý do kỹ thuật đằng sau quy tắc này:
- Tag là con trỏ, không phải nội dung. Cùng một tag có thể được push đè nhiều lần. Định danh nội dung thật là digest
sha256:...; bạn thấy nó trongdocker images --digests, và có thể ghim image bằngghcr.io/org/app@sha256:...khi cần tuyệt đối chắc chắn. imagePullPolicycủa K8s đối xử vớilatestkhác. Nếu tag làlatest(hoặc bỏ trống), K8s mặc địnhimagePullPolicy: Always, tức mỗi lần Pod khởi động là pull lại. Với tag khác, mặc định làIfNotPresent. Kết hợp với điểm 1,latestbiến mỗi lần restart Pod thành một lần deploy ngẫu nhiên.- Deployment chỉ rollout khi Pod template đổi. Nếu bạn push
latestmới rồikubectl applycùng file YAML, không có gì đổi trong template nên K8s không làm gì cả. Bạn tưởng đã deploy nhưng Pod vẫn chạy bản cũ. Tag bất biến đổi theo mỗi bản build nên rollout luôn xảy ra, vàkubectl rollout historycó ý nghĩa.
Cách lấy tag từ Git, dùng cả ở local lẫn CI:
TAG=sha-$(git rev-parse --short HEAD)
docker build -t ghcr.io/ten-github/my-app:$TAG .
Đẩy image lên GitHub Container Registry (miễn phí)
# 1. Tạo Personal Access Token trên GitHub với quyền write:packages (và read:packages)
# 2. Đăng nhập
echo $GITHUB_TOKEN | docker login ghcr.io -u TEN_GITHUB --password-stdin
# 3. Gắn tag đúng chuẩn registry
docker tag my-app:v1 ghcr.io/ten-github/my-app:v1.0.0
# 4. Đẩy lên
docker push ghcr.io/ten-github/my-app:v1.0.0
# 5. Kiểm tra bằng cách kéo về
docker pull ghcr.io/ten-github/my-app:v1.0.0
--password-stdin đọc token từ stdin thay vì đưa vào tham số dòng lệnh, để token không lọt vào lịch sử shell. Với GitHub Actions ở phần 16, GITHUB_TOKEN có sẵn, không cần tạo PAT. Sau khi push, package xuất hiện ở tab Packages trên profile GitHub của bạn; kiểm tra visibility ở đó (package mới có thể là private tuỳ cài đặt tài khoản, hãy xem trong settings của package).
docker tag không copy gì, nó chỉ gắn thêm một tên vào cùng image ID. Bạn thấy hai dòng trong docker images với cùng IMAGE ID.
Registry riêng tư và imagePullSecrets
Nếu image để private, cluster cần credential để pull. Bạn tạo một Secret kiểu docker-registry và khai báo trong Pod:
kubectl create secret docker-registry ghcr-creds \
--docker-server=ghcr.io \
--docker-username=TEN_GITHUB \
--docker-password=$GITHUB_TOKEN \
-n production
spec:
imagePullSecrets:
- name: ghcr-creds
containers:
- name: web
image: ghcr.io/ten-github/my-app:v1.0.0
Secret này nằm trong namespace, nên mỗi namespace deploy app private cần một bản. Với cluster cloud dùng registry cùng nhà (EKS với ECR, GKE với Artifact Registry), node thường được cấp quyền pull sẵn qua IAM và bạn không cần imagePullSecrets; kiểm tra với team DevOps.
Cạm bẫy: lỗi
ImagePullBackOffcó đúng 4 nguyên nhân: (1) sai tên image hoặc tag (gõ nhầm, tag chưa push), (2) image private mà thiếuimagePullSecrets, (3) token hết hạn hoặc thiếu quyềnread:packages, (4) node không có mạng ra registry. Luôn kiểm tra theo thứ tự này. Lệnh chẩn đoán làkubectl describe pod <ten>và đọc dòngFailed to pull imageở phần Events: nó nói rõnot found(nguyên nhân 1),unauthorized/denied(nguyên nhân 2–3), haytimeout(nguyên nhân 4). Với kind ở local còn nguyên nhân thứ 5: quênkind load docker-image, phần 5 nói tiếp.
Docker Compose: bước đệm trước K8s
Compose cho phép chạy nhiều container cùng lúc bằng một file YAML. Nó không phải K8s nhưng dùng khái niệm gần giống, và là bước tập dượt rất tốt: bạn sẽ thấy “gọi nhau bằng tên”, “tách cấu hình ra biến môi trường”, “volume để giữ dữ liệu” ở dạng đơn giản nhất trước khi gặp phiên bản K8s của chúng.
services:
frontend:
build: ./frontend
ports:
- '8080:80'
depends_on:
- api
api:
image: node:20-alpine
working_dir: /app
volumes:
- ./api:/app
command: npm start
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
ports:
- '3000:3000'
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker compose up -d # khởi động tất cả
docker compose logs -f frontend # log của một service
docker compose ps # trạng thái
docker compose exec api sh # chui vào một service
docker compose down # dừng và xoá container (volume pgdata vẫn còn)
docker compose down -v # xoá cả volume
Đọc file này với con mắt sắp học K8s:
frontenddùngbuild: ./frontend(Compose tự build từ Dockerfile trong thư mục đó),apivàdbdùngimage:có sẵn. Ở K8s không cóbuild: mọi thứ phải là image trên registry, vì cluster không có source code của bạn.ports: "8080:80"cùng nghĩa với-p 8080:80củadocker run; nháy kép để YAML không hiểu8080:80là số kiểu giờ:phút (bẫy YAML 1.1 ở phần 3).volumes: ./api:/appmount source code từ máy bạn vào container để sửa code là thấy ngay, đúng kiểu dev. Production không làm thế; K8s cũng không có khái niệm “thư mục máy bạn”.depends_onchỉ quyết định thứ tự khởi động, không chờ service kia sẵn sàng.apicó thể lên trước khi Postgres nhận kết nối và crash lần đầu. Compose xử lý bằnghealthcheck+depends_on: { db: { condition: service_healthy } }; K8s xử lý bằng readiness probe và init container (phần 6 và 8).
Ghi chú, liên hệ tới K8s: hãy để ý service
apigọi database bằng têndbtrongDATABASE_URL, không phải IP. Compose tạo một network riêng cho project và một DNS nội bộ ánh xạ tên service thành IP container. Đó chính xác là cách Service DNS hoạt động trong K8s, chỉ khác là ở quy mô nhiều máy. Nắm được điều này ở Compose, bạn sẽ hiểu Service của K8s trong 5 phút ở phần 7.
Compose và Kubernetes: bảng đối chiếu
| Docker Compose | Kubernetes tương đương |
|---|---|
services: | Deployment + Service |
ports: | Service + Ingress |
environment: | ConfigMap / Secret |
volumes: | PersistentVolumeClaim (hoặc emptyDir / configMap) |
deploy.replicas | spec.replicas |
depends_on + healthcheck | Readiness probe + init container |
| Chạy trên 1 máy | Chạy trên nhiều máy, tự phục hồi, tự cân bằng tải |
Dòng cuối là lý do K8s tồn tại: Compose không biết làm gì khi máy chủ chết, khi cần 20 bản sao trên 5 máy, hay khi muốn thay version mà không có giây nào downtime. Phần 5 mở đầu bằng đúng bốn tình huống đó. Nếu bạn muốn dùng Compose nghiêm túc cho môi trường dev của team (profiles, healthcheck, nhiều file), hai bài Compose fundamentals và Compose in depth trên blog là tài liệu đầy đủ; cho series này, mức ở trên là đủ.
Thực hành 1.6 và 1.7
Hai bài tập ngắn khép lại chặng nền tảng.
Đẩy image lên ghcr.io (bắt buộc, phần 5 sẽ dùng):
- Tạo PAT trên GitHub (Settings → Developer settings → Personal access tokens) với quyền
write:packages. Lưu vào biến shell:export GITHUB_TOKEN=...(không commit, không dán vào file trong repo). - Đăng nhập, gắn tag, push và pull lại theo năm bước ở mục registry, dùng image
my-app:v1từ Thực hành 1.5 và tagv1.0.0. - Mở tab Packages trên GitHub, xác nhận package
my-appxuất hiện với tagv1.0.0.
Kết quả mong đợi:
docker pushin các dòngPushedcho từng layer và kết thúc bằng digestsha256:....docker pullsau đó báoImage is up to date. Package hiện trên GitHub.
Compose (tuỳ chọn nếu bạn chỉ có 2 tuần):
- Tạo thư mục
lab-compose/vớidocker-compose.ymlở trên. Trongapi/, tạo một app Express tối giản có route/healthtrảokvàpackage.jsonvới scriptstart; thư mụcfrontend/dùng lại app Vite kèm Dockerfile của Thực hành 1.5. docker compose up -d, rồidocker compose ps: ba service đềurunning.docker compose exec api sh, bên trong gõnslookup dbvàwget -O- http://db:5432(sẽ báo lỗi giao thức nhưng chứng tỏ têndbphân giải được và cổng mở). Gõenv | grep DATABASEđể thấy biến được nạp.docker compose down, rồiup -dlại: dữ liệu Postgres vẫn còn nhờ volumepgdata. Cuối cùngdocker compose down -vđể dọn sạch.
Kết quả mong đợi:
nslookup dbtrả về một IP dạng172.x.x.x.envtrong containerapicóDATABASE_URLvới host làdb. Bạn vừa thấy service discovery bằng tên, thứ K8s làm cho bạn ở phần 7.
Kết thúc chặng nền tảng: bạn phải làm được
- Gõ thành thạo hơn 20 lệnh Linux cơ bản (phần 2).
- Giải thích được reverse proxy và vì sao SPA cần
try_files(phần 2 và bài này). - Viết YAML không lỗi thụt lề (phần 3).
- Viết Dockerfile multi-stage cho app FE, image dưới 50 MB.
- Build, chạy, exec vào container, đọc log.
- Push image lên ghcr.io.
Nếu còn gạch đầu dòng nào chưa làm được, hãy quay lại trước khi sang Phần 5. Từ đó trở đi, mọi lỗi “K8s” thực ra là một trong sáu điều này chưa vững.
Tóm tắt
- App frontend có pha build (cần Node, 500 MB
node_modules) và pha chạy (nginx + 2 MBdist). Multi-stage build tách hai pha, cho image khoảng 25 MB thay vì 1.2 GB. CMD ["nginx", "-g", "daemon off;"]dạng exec form: tiến trình chính phải chạy foreground và làm PID 1 để nhậnSIGTERM.nginx.confcho SPA có bốn việc:try_filesfallback vềindex.html(hết 404 khi F5),/assets/cache một nămimmutable,index.htmlkhông cache,/healthzcho probe..dockerignoreloạinode_modules,dist,.env: tránh lỗi native module, build chậm và lộ secret vào image.- Tên image có bốn phần: registry / owner / tên : tag. Không dùng
:latesttrên production; tag bất biến (v1.2.3,sha-a1b2c3d) để rollout luôn xảy ra và rollback được. - Image private cần Secret
docker-registryvàimagePullSecrets.ImagePullBackOffcó bốn nguyên nhân, kiểm tra theo thứ tự: tên/tag, thiếu secret, token, mạng. - Compose là K8s thu nhỏ trên một máy: service gọi nhau bằng tên, cấu hình qua
environment, dữ liệu qua volume. Điều Compose không làm được (nhiều máy, tự phục hồi, zero-downtime) là lý do học K8s.
Đọc thêm
- Dockerfile reference — đặc biệt mục
COPY --from,CMDexec form, và.dockerignore. - nginx documentation —
try_files,expires,add_header,gzip. - Docker Compose —
depends_onvớicondition,healthcheck, profiles. - Tối ưu và bảo mật image trên blog này: chạy non-root, quét lỗ hổng, giảm dung lượng thêm nữa.