GĐ15 — DevOps & vận hành: Docker, CI/CD, deploy
Study note cho FE engineer (mạnh JS/TS) chuyển sang Backend. Mục tiêu: tự đóng gói, tự deploy, tự debug prod mà không phải chờ DevOps.
Kiểm chứng ngày 2026-10-05: Node 24 là LTS khuyến nghị (Node 20 đã EOL 2026-04-30) nên Dockerfile dùng
node:24-alpine; Vercel Functions với Fluid compute mặc định 300 s (tối đa 800 s ở Pro/Enterprise); các workflow ở mục 8 và ví dụ deploy ECS ở cuối stage dùngactions/checkout@v7,actions/setup-node@v7(Node 24),aws-actions/configure-aws-credentials@v6,docker/build-push-action@v7. Image dữ liệu:postgres:18-alpine(volume ở/var/lib/postgresql) vàredis:8-alpine, tag đối chiếu trên Docker Hub (postgres, redis). Vòng đời chứng chỉ TLS ở mục 10 theo hai lịch riêng (CA/B Forum và Let's Encrypt). Ở lần cập nhật này daemon Docker không chạy: các lệnhdocker,docker composevà workflow mới thêm là code tham chiếu, chưa chạy; chỗ nào đã chạy thật thì có ghi "Đã chạy".
1. Vì sao backend dev cần biết DevOps cơ bản#
Định nghĩa. DevOps là tập practice ghép "Development" và "Operations": code, đóng gói, ship, chạy, giám sát — coi tất cả như một vòng đời liên tục thay vì "dev viết xong ném cho ops".
Tại sao quan trọng. Ở FE bạn npm run build ra file tĩnh, đẩy lên CDN là xong. Backend thì khác: service phải chạy liên tục, giữ kết nối DB, xử lý concurrency, có thể crash lúc 2h sáng. Nếu bạn không hiểu môi trường chạy, mỗi bug prod bạn đều phải chờ người khác. Team nhỏ / startup thường không có DevOps riêng — backend dev tự lo luôn.
Cơ chế (những việc thực tế bạn sẽ làm).
- Đóng gói app thành artifact chạy được ở mọi máy (Docker image).
- Định nghĩa pipeline: commit → test → build → deploy tự động (CI/CD).
- Cấu hình môi trường (env vars, secrets) tách khỏi code.
- Đọc log prod, kiểm tra health, rollback khi hỏng.
Ví dụ ngắn. Vòng lặp tối thiểu bạn cần tự chạy được:
Pitfall / case thực tế. "Chạy được trên máy tôi" là câu nói kinh điển gây cháy. Nguyên nhân gần như luôn là môi trường lệch: Node version khác, thiếu env var, thiếu system lib. DevOps cơ bản (Docker + env chuẩn) chính là để diệt câu này. Bug prod đầu đời của bạn thường không phải bug logic mà là bug cấu hình môi trường — biết đọc log và biết env đang là gì sẽ cứu bạn.
2. Container vs VM — Docker là gì, image vs container#
Định nghĩa.
- VM (Virtual Machine): ảo hóa cả phần cứng — mỗi VM chạy một OS đầy đủ (kernel riêng). Nặng (GB), boot chậm (phút).
- Container: ảo hóa ở tầng OS — nhiều container dùng chung kernel của host, chỉ cô lập process, filesystem, network. Nhẹ (MB), start trong mili giây.
- Docker: công cụ phổ biến nhất để build và chạy container.
Tại sao quan trọng. Container cho bạn "đóng gói cả môi trường" (Node runtime + code + system deps) thành một artifact bất biến. Ship image đó đi đâu cũng chạy giống hệt → diệt "works on my machine". Nhẹ hơn VM nên chạy được nhiều service/1 máy, scale nhanh.
Cơ chế.
- Image: template bất biến, read-only, gồm nhiều layer xếp chồng (mỗi lệnh Dockerfile = 1 layer). Giống "class" trong OOP.
- Container: một instance đang chạy của image, có thêm một lớp writable ở trên. Giống "object". Từ 1 image chạy được N container.
- Layer được cache và share giữa các image → tiết kiệm disk và thời gian build.
Ví dụ ngắn.
Pitfall / case thực tế.
- Nhầm image vs container: sửa file trong container đang chạy rồi
docker rm→ mất sạch, vì layer writable không được lưu vào image. Muốn lưu phảidocker commit(không nên) hoặc sửa Dockerfile rồi build lại (đúng). - Container không phải VM: nó chia sẻ kernel host. Image Linux chạy trên macOS/Windows thực chất chạy trong một Linux VM ẩn (Docker Desktop) → nhớ khi debug performance.
3. Dockerfile cho Node#
Định nghĩa. Dockerfile là script khai báo cách build một image: base image nào, copy gì, chạy lệnh gì, mở port nào, start ra sao.
Tại sao quan trọng. Đây là "recipe" quyết định image nặng/nhẹ, build nhanh/chậm, an toàn/không. Viết sai thứ tự → mỗi lần đổi 1 dòng code phải cài lại toàn bộ node_modules (chậm khủng khiếp trong CI).
Cơ chế (điểm mấu chốt: layer caching). Docker cache theo từng lệnh. Nếu input của một lệnh không đổi so với lần build trước, nó reuse cache. Cache bị "vỡ" từ lệnh đầu tiên có thay đổi trở xuống. → Đặt phần ít thay đổi (dependencies) TRƯỚC phần hay thay đổi (source code).
Ví dụ ngắn.
Dockerfile chỉ coi # là comment khi nó đứng đầu dòng; viết # sau một lệnh (trừ RUN, nơi shell tự bỏ qua) thì nó thành đối số, nên các comment ở EXPOSE/CMD được đặt trên dòng riêng.
Giải thích từng phần.
node:24-alpine: Alpine là distro Linux siêu nhẹ (~5MB base) → image nhỏ. Pin major (24, dòng LTS) thay vìlatestđể build reproducible.RUN npm run build: image chỉ chạy đượcdist/server.jssau khi compile..dockerignore(mục 5) loạidistkhỏi build context nêndisttrên máy bạn không vào image; thiếu bước này container thoát ngay vớiCannot find module '/app/dist/server.js'. Ví dụ này còn nguyên devDependencies; mục 4 tách chúng ra khỏi image cuối.npm cithay vìnpm install:cicài chính xác theopackage-lock.json, xóanode_modulescũ trước, nhanh và deterministic — chuẩn cho CI/Docker.EXPOSE: chỉ là metadata/tài liệu, KHÔNG mở port ra ngoài. Muốn truy cập vẫn phải-p 3000:3000khidocker run.CMDdạng exec (["node", ...]) tốt hơn dạng shell (node ...): process Node là PID 1 và nhận SIGTERM trực tiếp. Nhận được chưa có nghĩa là xử lý đúng: PID 1 không có handler thì SIGTERM bị bỏ qua (xem mục 12), nên app vẫn phải tự lắng nghe SIGTERM mới graceful shutdown được.
Pitfall / case thực tế.
COPY . .TRƯỚCnpm ci→ mỗi lần đổi 1 dòng code, cachenpm civỡ, cài lại toàn bộ deps. Đây là lỗi #1 làm CI chậm.- Alpine dùng
musllibc thay vìglibc→ vài native module (bcrypt, sharp, node-gyp) build lỗi. Khi đó dùngnode:24-slim(Debian) hoặc cài build tools. - Dùng
CMD npm start(dạng shell) → npm là PID 1, nuốt signal → container không shutdown gọn, deploy bị treo.
4. Multi-stage build#
Định nghĩa. Một Dockerfile chứa nhiều FROM (nhiều "stage"). Stage build dùng để compile/bundle; stage runtime chỉ copy kết quả sang, bỏ hết công cụ build.
Tại sao quan trọng. Image production nên chỉ chứa thứ cần để chạy, không cần TypeScript compiler, devDependencies, source .ts, test... Multi-stage giảm size (vài trăm MB → vài chục MB), giảm bề mặt tấn công (ít package = ít CVE), và không vô tình ship secret/dev tool.
Cơ chế. Mỗi stage có filesystem riêng. COPY --from=<stage> bốc artifact từ stage trước. Image cuối cùng = chỉ stage cuối; các stage trung gian bị bỏ.
Ví dụ ngắn.
Pitfall / case thực tế.
- Quên
--omit=devở stage runtime → vẫn shiptypescript,@types/*,jest... image phình to. - Copy nhầm
node_modulestừ builder (có cả devDeps) sang runtime → mất tác dụng multi-stage. Nên copydist/và cài lại prod deps riêng. - Native module cần compile: nếu build ở builder rồi copy
node_modulessang runtime, phải chắc hai stage cùng OS/arch, nếu không binary không tương thích.
Cache mount, secret mount và quét lỗ hổng cho image#
Ba thứ này bổ sung cho multi-stage, không thay nó. Code dưới đây là code tham chiếu, chưa chạy (lần soát này không có daemon Docker).
- Cache mount (
--mount=type=cache,target=/root/.npm):/root/.npmlà chỗ npm cache mặc định, mount này giữ nó giữa các lần build mà không ghi vào layer (Docker: tối ưu cache, đọc 2026-10-05). Runner CI mới mỗi lần thì mount này trống nếu không có nơi lưu cache của BuildKit; cách cấu hình chưa xác minh. - Secret mount (
--mount=type=secret,id=...): mặc định file nằm ở/run/secrets/<id>,targetđổi đường dẫn. Secret chỉ tồn tại trong lệnhRUNđó và không vào image cuối, khácARG/ENVvốn còn lại trong image (Docker: build secrets, đọc 2026-10-05). Chỉ cần khinpm cikéo package từ registry riêng; stage runtime chạynpm ci --omit=devcũng cần mount tương tự. - Quét lỗ hổng:
docker scout cves --only-severity critical,high --exit-code app:cichỉ liệt kê mức critical và high;--exit-codetrả mã2khi có lỗ hổng nên CI fail được (Docker Scout CLI, đọc 2026-10-05). Mã thoát khi kết hợp hai cờ này chưa xác minh: chạy thử một lần trong CI trước khi dùng nó làm cổng chặn.
5. .dockerignore#
Định nghĩa. File liệt kê những gì KHÔNG copy vào build context (tương tự .gitignore).
Tại sao quan trọng. COPY . . copy toàn bộ thư mục. Nếu không loại trừ, node_modules local (thường vài trăm MB, còn có thể là binary sai OS) và .git, .env bị đẩy vào image → build chậm, image to, LỘ SECRET.
Cơ chế. Docker đọc .dockerignore khi tính build context (thứ gửi tới daemon). File match bị loại trước cả khi COPY chạy → build nhanh hơn và cache ổn định hơn.
Ví dụ ngắn.
Pitfall / case thực tế.
- Không có
.dockerignore→ copynode_moduleslocal (macOS binary) vào image Linux → app crash runtime với lỗi native module. Đây là bẫy kinh điển của người mới. - Quên ignore
.env→ secret đi thẳng vào image layer, aidocker history/ pull image đều đọc được. Secret trong layer KHÔNG xóa được bằng cách xóa file ở layer sau.
6. docker-compose — nhiều service#
Định nghĩa. docker-compose (nay là docker compose) khai báo và chạy nhiều container liên quan bằng một file YAML: app + database + cache + queue... với một lệnh docker compose up.
Tại sao quan trọng. App backend hiếm khi đứng một mình — nó cần Postgres, Redis... Compose cho bạn dựng nguyên stack local giống prod, reproducible, share được cho cả team (chỉ cần git clone + docker compose up). Thay thế cho "cài Postgres thủ công lên máy" (mỗi người một version, hỏng lung tung).
Cơ chế.
- services: mỗi service = 1 container (hoặc scale nhiều).
- networks: compose tự tạo network chung; các service gọi nhau bằng tên service làm hostname (DNS nội bộ). App connect Postgres qua
postgres:5432, không phảilocalhost. - volumes: mount dữ liệu bền (DB data) ra ngoài container để không mất khi container bị xóa.
- depends_on: thứ tự khởi động (chỉ đảm bảo start trước, KHÔNG đảm bảo ready).
- environment / env_file: truyền env vào container.
Ví dụ ngắn.
Image Postgres 18 đổi đường dẫn volume. Từ PostgreSQL 18, image chính thức đặt PGDATA theo phiên bản (/var/lib/postgresql/18/docker) và khai báo VOLUME là /var/lib/postgresql, không còn /var/lib/postgresql/data như bản 17 trở xuống (trang image, đọc ngày 2026-10-05). Trang image yêu cầu mount theo vị trí mới. Suy luận từ khai báo VOLUME (chưa chạy vì máy soạn không có Docker daemon): mount pgdata vào đường dẫn cũ /var/lib/postgresql/data sẽ không chứa PGDATA của image 18, nên dữ liệu có thể nằm ở volume ẩn danh và mất khi docker compose down rồi up. Volume pgdata đã tạo bởi image 16 cũng không tự nâng lên 18: ở lab thì xoá volume (docker compose down -v) rồi dựng lại; dữ liệu cần giữ thì dùng pg_upgrade.
Sơ đồ: mạng compose, tên service là hostname
Pitfall / case thực tế.
- App connect
localhost:5432thay vìpostgres:5432→ fail. Trong compose network,localhostlà chính container đó, không phải host. Phải dùng tên service. depends_onKHÔNG đợi DB sẵn sàng nhận query — chỉ đợi container start. App boot nhanh hơn Postgres init → crash "connection refused". Fix: dùngcondition: service_healthy+ healthcheck, hoặc retry logic trong app.- Quên volume cho Postgres →
docker compose downlà mất sạch data.
7. Environment tách biệt — 12-factor & secrets#
Định nghĩa. Cùng một codebase/image chạy được ở nhiều môi trường (dev / staging / prod), khác nhau CHỈ ở config truyền qua environment variables. Đây là factor III của phương pháp 12-Factor App.
Tại sao quan trọng. Không được hardcode DB url, API key, port theo môi trường vào code. Nếu hardcode, bạn phải build image khác nhau cho mỗi env → không đảm bảo "cái đã test ở staging chính là cái chạy ở prod". Tách config → 1 image, N môi trường, an toàn và reproducible.
Cơ chế.
- Code đọc config qua
process.env.DATABASE_URL, có validate lúc boot. - dev: file
.envlocal (gitignored). - staging/prod: env vars inject bởi platform (Railway/Render dashboard, GitHub Secrets, Docker secrets, Vault...).
- Secrets management: giá trị nhạy cảm (DB pass, JWT secret, API key) KHÔNG bao giờ commit; lưu ở secret store của platform, chỉ inject lúc runtime.
Ví dụ ngắn.
NODE_ENV chỉ nhận development / test / production (Jest/Vitest tự đặt test, nhiều thư viện đổi hành vi theo giá trị này). Staging vẫn chạy NODE_ENV=production để đi đúng đường code của prod; thứ phân biệt staging với prod là APP_ENV. Đã chạy thử schema này trên zod 4.6: postgres://... qua z.url(), NODE_ENV=staging bị từ chối.
Pitfall / case thực tế.
- Commit
.envthật lên Git → secret lộ vĩnh viễn trong lịch sử (xóa file ở commit sau KHÔNG đủ, phải rotate key + rewrite history). Luôn.gitignore.env, chỉ commit.env.example. - Không validate env → app boot, chạy tới request đầu tiên mới chết vì
DATABASE_URLundefined. Validate fail-fast lúc boot tốt hơn nhiều. - Bỏ qua staging, deploy thẳng lên prod → không có nơi test migration/config an toàn.
8. CI/CD — pipeline & GitHub Actions#
Định nghĩa.
- CI (Continuous Integration): mỗi lần push, tự động chạy lint + test + build để phát hiện lỗi sớm.
- CD (Continuous Delivery/Deployment): tự động (hoặc 1 nút bấm) đưa code đã pass lên môi trường chạy.
- Pipeline: chuỗi bước tự động chạy theo trigger (push, PR, tag).
Tại sao quan trọng. Không ai muốn "test tay rồi FTP lên server". Pipeline biến quy trình ship thành tự động, nhất quán, có kiểm soát. PR không pass CI thì không merge → chất lượng bảo vệ tự động. Đây là xương sống của làm việc nhóm hiện đại.
Cơ chế. GitHub Actions: file YAML trong .github/workflows/. Định nghĩa on (trigger), jobs (chạy song song/tuần tự), mỗi job có steps. Runner (máy ảo GitHub cấp) checkout code rồi chạy từng step. Cache deps giữa các lần chạy để nhanh hơn.
Ví dụ ngắn.
Pitfall / case thực tế.
- Không cache deps → mỗi run
npm citải lại từ đầu, pipeline chậm gấp đôi.cache: npmhoặcactions/cachekhắc phục. - Deploy chạy song song với test (không
needs: test) → ship code chưa pass test lên prod. - Đổ secret vào log (
echo $JWT_SECRET) → lộ trong CI log public. Dùng${{ secrets.X }}và không in ra. - CI xanh nhưng dùng Node version khác local → bug chỉ hiện ở CI. Pin version giống nhau ở mọi nơi (kể cả Dockerfile).
Test với Postgres thật bằng service container#
Test tích hợp cần Postgres thật (mock DB che mất lỗi SQL). GitHub Actions dựng DB cạnh job bằng services; job chạy thẳng trên runner nên phải map port và nối qua localhost (GitHub Docs: PostgreSQL service containers, đọc 2026-10-05). Đây là phương án thay cho Testcontainers của GĐ13 (mục 14): chọn một trong hai, không chạy cả hai. Code tham chiếu, chưa chạy trên GitHub (cú pháp YAML đã parse thử bằng thư viện yaml; chưa lint bằng actionlint).
options với --health-cmd làm GitHub chờ Postgres healthy rồi mới chạy step đầu, nên bước migrate không đụng DB chưa sẵn sàng. Với cách này không có globalSetup của Testcontainers cấp URL qua inject: test/db.ts phải đọc DATABASE_URL (và vẫn gọi guard _test của GĐ13 trên URL đó), nếu không test sẽ nối vào một DB khác với DB vừa migrate. Chạy migration trong CI trước test cũng kiểm luôn rằng chuỗi migration áp được lên DB trống.
9. Reverse proxy (Nginx)#
Định nghĩa. Reverse proxy là server đứng TRƯỚC app, nhận request từ client rồi forward tới app backend phía sau. Nginx là lựa chọn phổ biến.
Tại sao quan trọng. Không nên expose Node process trực tiếp ra Internet. Node giỏi xử lý logic nhưng không tối ưu cho việc terminate TLS, serve static, chống slow-client, load balance. Đặt Nginx phía trước để nó lo mấy việc đó, Node chỉ tập trung business logic.
Cơ chế.
- proxy_pass: chuyển request tới upstream (app Node ở
localhost:3000). - Load balancing: phân phối request cho nhiều instance Node (round-robin, least-conn).
- TLS termination: Nginx giải mã HTTPS, nói HTTP với app phía sau → app không phải xử lý cert.
- Thêm: serve static file, gzip, rate limit, buffer request chống slowloris.
Ví dụ ngắn.
Sơ đồ: đường đi của một request qua proxy
Pitfall / case thực tế.
- Quên
X-Forwarded-For/X-Forwarded-Proto→ app thấy mọi request đến từ IP của Nginx (rate-limit sai) và tưởng là HTTP (redirect loop). Nhớapp.set('trust proxy', true)ở Express. - Expose Node trực tiếp port 80/443 và tự xử TLS trong app → mất tối ưu, mỗi lần renew cert phải restart app.
- Nếu dùng platform PaaS (Railway/Render) thì reverse proxy + TLS đã được lo sẵn — không cần tự dựng Nginx. Chỉ cần khi tự quản VPS.
Location riêng cho SSE và stream LLM#
Cấu hình location / ở trên hỏng với stream: Nginx mặc định buffer response (proxy_buffering on), nên từng token LLM bị giữ lại và client nhận một cục khi xong; còn proxy_read_timeout mặc định 60s cắt kết nối nếu app im quá 60 giây giữa hai lần ghi (Nginx: ngx_http_proxy_module, đọc 2026-10-05). Tách một location cho đường stream (code tham chiếu, chưa chạy vì máy soạn không có Nginx):
- Cách thay thế phía app: gửi header
X-Accel-Buffering: notrong response (Express:res.setHeader("X-Accel-Buffering", "no")); Nginx tắt buffer cho response đó trừ khi cóproxy_ignore_headers X-Accel-Buffering. Cách này đi cùng code, nên không phụ thuộc người sửa file Nginx. - Đừng chép cấu hình WebSocket sang SSE: WebSocket dùng
map $http_upgrade $connection_upgraderồiUpgrade/Connection; SSE là HTTP thường nên chỉ cầnConnection ""như trên. - Heartbeat: dòng comment SSE (
: ping) mỗi 15 đến 30 giây giữ cho cả Nginx lẫn load balancer phía trước không coi kết nối là rảnh.
10. HTTPS/TLS ở prod#
Định nghĩa. TLS mã hóa traffic giữa client và server (HTTPS = HTTP over TLS). Cert do một CA (Certificate Authority) ký; Let's Encrypt là CA miễn phí, tự động.
Tại sao quan trọng. Không HTTPS = password/token bay dạng plaintext, browser cảnh báo "Not Secure", nhiều API (cookie Secure, service worker, HTTP/2) yêu cầu HTTPS. Bắt buộc ở prod.
Cơ chế.
- Let's Encrypt + Certbot: tự xin cert bằng ACME challenge (chứng minh bạn sở hữu domain), tự động renew qua cron. Hạn cert mặc định hiện vẫn là 90 ngày nhưng sắp ngắn lại (xem "Hạn cert đang ngắn dần" bên dưới), nên renew tự động và cảnh báo hết hạn không còn là tuỳ chọn.
- Platform lo hộ: Railway, Render, Vercel, Cloudflare tự cấp và renew cert cho domain bạn gắn — bạn không đụng gì.
Ví dụ ngắn.
Pitfall / case thực tế.
- Cert hết hạn vì renew cron chết → prod sập với lỗi cert. Set alert theo dõi ngày hết hạn.
- Terminate TLS ở Nginx rồi để app tưởng client dùng HTTP → tạo redirect
http→httpsvô hạn. ĐọcX-Forwarded-Protothay vì tự đoán. - Với platform managed thì đừng tự cài Certbot — sẽ xung đột với TLS của platform.
Hạn cert đang ngắn dần#
Có hai lịch khác nhau, đừng trộn: một là trần do CA/Browser Forum đặt cho mọi CA, một là lịch riêng của Let's Encrypt.
| Nguồn | Mốc | Hạn tối đa / hạn cấp |
|---|---|---|
| CA/Browser Forum (ballot SC-081v3) | từ 2026-03-15 | 200 ngày |
| từ 2027-03-15 | 100 ngày | |
| từ 2029-03-15 | 47 ngày | |
Let's Encrypt, profile classic (mặc định) | hiện tại | 90 ngày |
| từ 2027-02-10 | 64 ngày | |
| từ 2028-02-16 | 45 ngày | |
Let's Encrypt, profile tlsserver (opt-in) | từ 2026-05-13 | 45 ngày |
Nguồn: Let's Encrypt: From 90 to 45 (đọc 2026-10-05); lịch CA/B Forum đọc qua bài tổng hợp SSL.com, chưa đọc văn bản ballot gốc (trang cabforum.org trả 404). Let's Encrypt còn có profile shortlived (6 ngày), nằm ngoài phạm vi bài này.
Hệ quả cho vận hành: đừng viết cứng chu kỳ renew kiểu "mỗi 60 ngày" (hỏng khi cert chỉ còn 45 ngày), để Certbot hoặc client ACME tự quyết, và bật cảnh báo khi cert sắp hết hạn mà chưa được renew. Bài của Let's Encrypt khuyên dùng ACME Renewal Information (ARI) và có monitoring cho việc này.
11. Deploy targets — PaaS vs VPS + Docker#
Định nghĩa.
- PaaS (Railway / Render / Fly.io): platform managed, bạn đẩy code/Dockerfile, họ lo build, chạy, TLS, scaling, log.
- VPS + Docker (DigitalOcean / Hetzner / EC2): bạn thuê máy trần, tự cài Docker, tự dựng compose/Nginx, tự lo mọi thứ.
Tại sao quan trọng. Chọn sai = tốn thời gian hoặc tốn tiền. Người mới nên bắt đầu bằng PaaS để ship nhanh; muốn hiểu sâu / kiểm soát chi phí thì chuyển VPS.
Cơ chế & trade-off.
| Tiêu chí | PaaS (Railway/Render) | VPS + Docker |
|---|---|---|
| Tốc độ lên sóng | Vài phút, git push là chạy | Chậm, phải setup server |
| TLS / proxy / scaling | Tự động | Tự dựng Nginx/Certbot |
| Kiểm soát | Ít (bị giới hạn theo platform) | Toàn quyền |
| Chi phí | Rẻ lúc nhỏ, đắt khi scale | Rẻ hơn khi tải lớn, cố định |
| Học được gì | Ít về ops | Hiểu sâu Linux/network/Docker |
| Debug | Log qua dashboard | SSH vào máy, full access |
Ví dụ ngắn.
Pitfall / case thực tế.
- Chọn VPS quá sớm cho MVP → tốn hàng ngày setup thay vì viết feature. Bắt đầu bằng PaaS, migrate sau khi có traffic/hiểu nhu cầu.
- PaaS free tier "ngủ" (cold start) sau thời gian idle → request đầu chậm vài giây. Biết để không hoảng khi demo.
- VPS: quên firewall/
ufw, để Postgres port 5432 mở ra Internet → bị scan và tấn công. Chỉ mở 80/443, DB chỉ nghe internal.
12. Zero-downtime, health check & log ở prod#
Định nghĩa.
- Zero-downtime deploy: cập nhật version mới mà user không thấy gián đoạn.
- Health check: hai endpoint để platform/proxy biết instance còn sống và sẵn sàng:
/health/live(process còn phản hồi, không chạm DB) và/health/ready(sẵn sàng nhận traffic, có kiểm dependency). - Log ở prod: cách quan sát app đang chạy (stdout/stderr → log aggregator).
Tại sao quan trọng. Nếu deploy = tắt app cũ rồi bật app mới, sẽ có khoảng chết + nếu app mới lỗi thì sập luôn. Health check + rolling update để traffic chỉ chuyển sang instance mới KHI nó đã healthy. Log là mắt của bạn ở prod — không có log = debug mù.
Cơ chế.
- Rolling / blue-green: khởi động instance mới → chờ nó pass health check → chuyển traffic → tắt instance cũ. Nếu mới fail health check thì giữ nguyên cũ (không sập).
- Health endpoint:
/health/livetrả 200 miễn là process còn trả lời, không phụ thuộc bên ngoài (platform dùng nó để quyết định restart)./health/readytrả 200 khi dependency (DB) OK, 503 khi chưa sẵn sàng hoặc đang tắt → LB không route tới. Tách đôi vì nếu liveness chạm DB, DB chậm sẽ khiến platform restart cả đàn instance (→ GĐ18 mục 5). - Graceful shutdown: nhận SIGTERM → ngừng nhận request mới, xử nốt request đang chạy, đóng queue/DB pool sau khi HTTP đã drain, rồi exit. Mẫu code duy nhất của repo (đúng thứ tự, có timer ép thoát) ở GĐ09 mục 18; phần dưới chỉ nói những gì riêng của Docker.
- Log: ghi ra stdout dạng JSON (structured), để platform/Docker gom.
docker logs, dashboard PaaS, hoặc chuyển tới Datadog/Grafana Loki.
Ví dụ ngắn.
Phần riêng của Docker khi shutdown (code drain: GĐ09 mục 18):
- PID 1 không có handler thì bỏ qua SIGTERM. Đã chạy
node:24-alpinevới server không đăng kýSIGTERM:docker stop -t 5chờ đủ 5 s rồi SIGKILL, container thoát với exit 137. Vì vậy app luôn phải tự đăng ký handler. - tini (
docker run --init,init: truetrong compose) không làm shutdown graceful. Nó chỉ reap process zombie và chuyển tiếp signal. Cùng server không handler, chạy sau tini thì dừng ngay với exit 143 (tín hiệu mặc định giết process), tức là vẫn cắt request đang chạy. Kubernetes không có cờ--init: image cần tự cài tini (RUN apk add --no-cache tini,ENTRYPOINT ["/sbin/tini", "--"]) hoặc để app làm PID 1 với handler đúng. STOPSIGNALmặc định là SIGTERM; chỉ đổi khi framework bắt buộc dùng signal khác.- Grace period phải lớn hơn thời gian drain:
docker stop -t <giây>,stop_grace_periodtrong compose,terminationGracePeriodSecondstrên Kubernetes (GĐ18 mục 6). Quá hạn thì runtime SIGKILL, dù app đang xử lý dở.
Pitfall / case thực tế.
/health/readychỉ trả200 OKcứng, không kiểm DB → instance mất kết nối DB vẫn nhận traffic → user gặp lỗi 500 hàng loạt. Readiness nên phản ánh dependency thật (nhưng đừng để check quá nặng gây flapping). Ngược lại, đừng đặt kiểm DB vào/health/live.- Không xử SIGTERM → deploy làm rớt request đang chạy (mất data/transaction dở). Luôn graceful shutdown (GĐ09 mục 18).
- Log ra file trong container → container xóa là mất log. Luôn log ra stdout, để hệ thống bên ngoài gom.
- Log không có request-id/timestamp/context → có log nhưng vẫn không lần được nguyên nhân. Dùng structured logging (pino/winston JSON).
Áp dụng — Dự án 3#
Mục tiêu: Dự án 3 chạy local bằng docker-compose, deploy qua CI/CD.
Local (docker-compose):
-
Viết
Dockerfilemulti-stage cho service Node (stage build → stage runtime,npm ci --omit=dev).Lời giải và cách kiểm tra
Sơ đồ — layer cache và vì sao thứ tự
COPYquan trọngtextReadyCode tham chiếu:
DockerfiletextReadyHEALTHCHECKdùng/health/live(không chạm DB), đúng quy ước mục 12. Nếu app dùng Prisma 7, thêm bướcnpx prisma generateở stagebuildvà copy thư mục client đã sinh sang stage runtime theooutputbạn cấu hình (xem GĐ05). -
Thêm
.dockerignore(node_modules,.git,.env,dist).Lời giải và cách kiểm tra
Code tham chiếu:
.dockerignoretextReady -
docker-compose.yml: servicesapp+postgres+redis, network mặc định, volumepgdata,depends_onvớicondition: service_healthy, healthcheck cho Postgres.Lời giải và cách kiểm tra
Code tham chiếu:
docker-compose.yml(secret đọc từ.env, không ghi vào file này)textReady -
Config qua env:
config.tsvalidate bằng zod;.env(gitignored) +.env.example(commit).Lời giải và cách kiểm tra
Đã chạy:
config.tsdưới đây, trên zod 4.6.5 với Node 24 (chạy trực tiếp bằng type stripping, ngoài Docker): đủ biến thì in cấu hình và thoát 0; thiếu biến,NODE_ENV=staginghoặcJWT_SECRETngắn thì in tên biến lỗi và thoát 1. Chưa chạy:.env.examplebên dưới và việc nạp nó quaenv_filecủa compose.Code tham chiếu:
config.tstypescriptReady.env.example(commit;.envthật nằm trong.gitignore). Compose ghi đèDATABASE_URLvà hai URL Redis bằng hostname service, nên các giá trịlocalhostdưới đây chỉ dùng khi chạy app ngoài Docker. File không đặtNODE_ENV:env_fileghi đèENV NODE_ENV=productioncủa Dockerfile, nên giá trị đó chỉ truyền ở dòng lệnh khi chạy ngoài Docker (NODE_ENV=development npm run dev).textReady -
docker compose up --build→ app connect DB qua hostnamepostgres, không phảilocalhost.Lời giải và cách kiểm tra
Kết quả mong đợi (nghiệm thu cục bộ):
bashReady
CI/CD (GitHub Actions):
-
Workflow
on: [push, pull_request]:checkout → setup-node (cache npm) → npm ci → lint → test → build.Lời giải và cách kiểm tra
Code tham chiếu:
.github/workflows/ci.ymltextReadyKhi job
deploydùngenvironment: production, claimsubcủa OIDC token chứaenvironment:productionthay choref:(xem trust policy ở phần Bổ sung); sửa trust policy cho khớp trước khi chạy thật. -
Job
deployvớineeds: testvàif: main→ push image /railway up/ ssh +docker compose up -d --build. -
Secrets qua
${{ secrets.* }}, không hardcode, không echo.
Prod:
- Deploy PaaS trước (Railway/Render) cho nhanh; TLS + proxy platform lo.
- Thêm
/health/live(không chạm DB) và/health/ready(check DB) + graceful shutdown (SIGTERM, mẫu ở GĐ09 mục 18). - Log structured ra stdout; xem qua dashboard.
Khung và mã dùng chung
Đây là đặc tả nên chỉ đưa khung, không viết cả dự án. Chưa chạy: toàn bộ khối dưới là code tham chiếu, chưa build bằng
Docker, chưa chạy workflow, chưa deploy. actionlint không có sẵn trên máy này nên chưa lint YAML của các khối trong lời giải; các tên input/phiên bản
đối chiếu theo bảng đã kiểm chứng ngày 2026-10-05 ở đầu stage. Mọi "kết quả mong đợi" là suy ra từ code và tài liệu.
Hướng làm. (1) Dockerfile multi-stage, chạy bằng user không phải root. (2) .dockerignore. (3) Compose có
Postgres khoẻ trước khi app và bước migrate chạy. (4) config.ts validate env (mục 7). (5) CI, rồi mới đến deploy.
Phạm vi lời giải. CI/CD yêu cầu 2–3 và Prod yêu cầu 1–3 không có lời giải riêng: job deploy với needs và if nằm trong workflow ở CI/CD yêu cầu 1 và workflow ECS ở phần Bổ sung; secrets qua ${{ secrets.* }} đã nêu ở mục 8; Prod là chọn nền tảng và kiểm health/log, đã phủ ở mục 11, mục 12 và Done khi.
Lỗi hay gặp: localhost:5432 trong container; depends_on không có condition nên app boot trước DB;
npx prisma migrate deploy trong image runtime không có Prisma CLI (đã --omit=dev), vì thế migrate dùng stage build;
quên stop_grace_period nên bị SIGKILL giữa chừng (mặc định compose chỉ chờ 10 s); quên ENV NODE_ENV=production ở
stage runtime nên framework chạy đường code của dev. CI: needs thiếu thì deploy chạy song song với test.
Sơ đồ — đường đi của một lần deploy
Done khi#
-
Giải thích được image vs container, container vs VM bằng lời của mình.
Đáp án
Image là template bất biến, nhiều layer, chỉ đọc; container là một instance đang chạy với lớp ghi mỏng ở trên (như class và object). VM ảo hoá cả phần cứng và chạy kernel riêng; container dùng chung kernel host, chỉ cô lập process, filesystem, network nên nhẹ và start nhanh. Sai thường gặp: sửa file trong container rồi
docker rmlà mất. Xem mục 2. -
Viết được Dockerfile multi-stage cho Node đúng thứ tự cache (COPY package.json trước,
npm ci, rồi COPY source).Đáp án
Stage build:
COPY package*.json→npm ci→COPY . .→npm run build. Stage runtime:npm ci --omit=dev+COPY --from=build /app/dist. Tự kiểm: sửa một dòng.tsrồi build lại, layernpm ciphải hiệnCACHED. Sai thường gặp:COPY . .đứng trướcnpm ci; copy cảnode_modulestừ builder. Xem mục 3 và mục 4. -
Có
.dockerignoreloạinode_modules/.env/.git.Đáp án
Có
node_modules,.git,.env,.env.*,dist,coverage. Tự kiểm:docker run --rm --entrypoint ls <image> -a /appkhông liệt kê.env(docker historychỉ hiện lệnh nhưCOPY . ., không chứng minh được gì về tên file); secret nằm trong layer thì xoá ở layer sau cũng không mất. Xem mục 5. -
docker compose updựng được app + Postgres + Redis, app connect DB qua tên service, data bền qua volume.Đáp án
App dùng hostname
postgres(khônglocalhost),depends_onvớicondition: service_healthy, volumepgdata. Tự kiểm:docker compose psthấyhealthy;docker compose downrồiup(không-v) dữ liệu còn.depends_onthường chỉ đảm bảo start, không đảm bảo ready. Xem mục 6. -
Config toàn bộ qua env (validate fail-fast),
.envgitignored, chỉ commit.env.example.Đáp án
config.tsparseprocess.envbằng zod lúc boot (NODE_ENV∈development/test/production,APP_ENVcho môi trường triển khai);.envtrong.gitignore, chỉ commit.env.example. Tự kiểm: xoáDATABASE_URLrồi chạy, process phải thoát ngay với lỗi rõ. Secret đã commit nhầm: rotate key, không chỉ xoá file. Xem mục 7. -
Có workflow GitHub Actions: lint → test → build, cache deps, deploy chỉ khi test pass và đúng branch.
Đáp án
actions/setup-node@v7vớicache: npm; jobdeploycóneeds: testvàif: github.ref == 'refs/heads/main'. Tự kiểm: mở PR thì job deploy bị skip; làm test đỏ thì deploy không chạy; lần chạy thứ hainpm cinhanh hơn nhờ cache. Xem mục 8. -
Hiểu vì sao không expose Node trực tiếp; biết reverse proxy + TLS termination làm gì (dù dùng platform lo hộ).
-
Chọn được deploy target phù hợp và nêu được trade-off PaaS vs VPS.
Đáp án
PaaS: ra mắt nhanh, ít kiểm soát, rẻ khi nhỏ; VPS + Docker: toàn quyền, rẻ hơn khi tải lớn, tự lo TLS/firewall/vá. MVP chọn PaaS; có yêu cầu kiểm soát hoặc chi phí thì mới sang VPS/ECS. Nêu được ít nhất một rủi ro mỗi bên (cold start với free tier; cổng DB mở ra Internet trên VPS). Xem mục 11.
-
Có
/health/live(không chạm DB) và/health/ready(phản ánh dependency), có graceful shutdown, biết xem log prod.Đáp án
/health/livekhông chạm DB (platform dùng để restart),/health/readykiểm DB và trả 503 khishuttingDown. Tự kiểm: dừng Postgres thìreadythành 503 cònlivevẫn 200; gửi SIGTERM khi đang có request chậm thì request đó vẫn hoàn tất, process thoát sau khi drain. Log JSON ra stdout, xem bằngdocker compose logs -f app. Xem mục 12 và GĐ09 mục 18. -
Deploy thật Dự án 3 lên một môi trường và truy cập được qua HTTPS.
Đáp án
Tự kiểm sau khi bạn tự deploy:
curl -sI https://<domain>/health/readytrả200, trình duyệt không cảnh báo chứng chỉ,http://chuyển sanghttps://không lặp vô hạn (cầnX-Forwarded-Proto), env thiếu thì deploy fail ở bước boot. Mục này đòi hỏi tài khoản và domain của chính bạn; tài liệu này không deploy hộ. Xem mục 11 và mục 12. -
Giải thích được vai trò của EC2, S3, IAM, RDS, Lambda, CloudWatch và VPC; dựng được sơ đồ mạng đơn giản cho API + database + upload.
Đáp án
EC2: máy ảo; S3: lưu object; IAM: danh tính và quyền; RDS: database managed; Lambda: hàm theo sự kiện; CloudWatch: log, metric, alarm; VPC: mạng riêng với subnet, route, security group. Sơ đồ tối thiểu: ALB ở public subnet → ECS/EC2 ở private subnet → RDS ở subnet database không có đường ra Internet; upload đi thẳng lên S3 bằng presigned URL, ứng dụng chỉ giữ khoá. Bài học đầy đủ và lab ở GĐ16.
Câu hỏi mở (tự trả lời sau)#
-
Khi nào cần Kubernetes thay vì docker-compose? (gợi ý: khi cần scale nhiều node, self-healing, service mesh — đa số dự án nhỏ CHƯA cần).
Hướng trả lời hiện tại
Kubernetes: khi có ít nhất hai trong ba điều kiện ở phần "Bước tiếp theo" (nhiều service triển khai độc lập, nhiều môi trường/vùng, công ty đã dùng K8s); một backend và một worker thì compose hoặc ECS Fargate đủ.
(Chưa chốt, không phải khẳng định chắc chắn.)
-
Chiến lược migration DB an toàn trong CD pipeline (chạy migration trước hay sau khi deploy app version mới)?
Hướng trả lời hiện tại
Migration trong CD: chạy migration trước khi bật version app mới (bước một lần, như service
migrateở lời giải Dự án 3), và viết migration tương thích với cả version cũ lẫn mới để rolling deploy không lỗi (xem GĐ14 mục 10). Thao tác huỷ (DROP) để lần deploy sau.(Chưa chốt, không phải khẳng định chắc chắn.)
-
Observability sâu hơn: metrics (Prometheus) + tracing (OpenTelemetry) khác gì với logging thuần?
Hướng trả lời hiện tại
Observability: log trả lời "chuyện gì đã xảy ra ở một request", metrics trả lời "hệ thống đang khoẻ đến đâu theo thời gian" (tỉ lệ lỗi, latency, độ bão hoà), tracing nối các bước của một request qua nhiều service. Bắt đầu bằng log có cấu trúc cùng request-id, thêm metrics và tracing khi có nhiều hơn một service hoặc cần đo SLO.
(Chưa chốt, không phải khẳng định chắc chắn.)
☁️ Bổ sung — Deploy Vercel & AWS (theo mục tiêu AI SaaS)#
Phần chính ở trên dùng Railway/Render/VPS. Đây là 2 nền tảng phổ biến nhất cho AI SaaS: Vercel (frontend + serverless) và AWS (backend/scale).
Vercel — cho Frontend (Next.js) & serverless nhẹ#
- Định nghĩa: platform tối ưu cho Next.js/frontend; deploy =
git push, auto build + CDN global + preview URL mỗi PR. - Tại sao dùng: FE của AI SaaS (Next.js) đặt ở Vercel là chuẩn mực; DX cực tốt, HTTPS/CDN tự lo.
- Cơ chế: mỗi push → build → deploy immutable; env vars set trong dashboard; serverless/edge functions cho API route nhẹ.
- Giới hạn quan trọng (case AI SaaS): serverless function có timeout (với Fluid compute, mặc định 300 s ở mọi plan, tối đa 800 s ở Pro/Enterprise; xem trang limits của Vercel trước khi chọn) và không giữ state giữa các request → một lần gọi LLM streaming vài chục giây vẫn chạy được, nhưng KHÔNG hợp cho tác vụ vượt trần thời gian, background job, WebSocket bền, kết nối DB pool lâu.
- → Kiến trúc thực tế: FE + BFF nhẹ trên Vercel, còn backend nặng (stream rất dài, queue, RAG ingest) đặt trên AWS/Railway — Vercel gọi sang.
- Pitfall: nhét cả backend nặng vào Vercel functions → timeout, cold start, cạn DB connection (dùng pooler như Neon/Supabase pooling).
AWS — cho Backend production & scale#
Lộ trình bảy dịch vụ AWS — EC2, S3, IAM, RDS, Lambda, CloudWatch và VPC — có bài học riêng tại GĐ16, gồm sơ đồ mạng, cấu hình quyền và ví dụ Node.js. GĐ15 tập trung vào lựa chọn nơi chạy app và quy trình deploy.
Các cách chạy Node backend, từ dễ → linh hoạt:
- Elastic Beanstalk / App Runner: managed, gần giống Railway; nhanh, ít cấu hình.
- ECS + Fargate (khuyên cho SaaS): chạy Docker container (đúng Dockerfile bạn học ở trên) không cần quản server; auto-scale theo CPU/req.
- EC2: VPS thuần, tự cài Docker/Nginx — hiểu sâu nhất nhưng tự quản nhiều.
- Lambda: serverless function; hợp task ngắn/event. Invocation có timeout tối đa 15 phút; response streaming có hỗ trợ nhưng phụ thuộc cách gọi, tích hợp và Region, nên phải kiểm tra các giới hạn của đường đi cụ thể.
Dịch vụ AWS đi kèm 1 AI SaaS thật:
- RDS (Postgres, bật pgvector) hoặc dùng Neon/Supabase ngoài AWS cho nhanh.
- ElastiCache (Redis) cho cache/queue.
- S3 lưu file user upload (tài liệu để RAG).
- CloudWatch logs/metrics/alarm; Secrets Manager giữ API key LLM/Stripe.
- ALB (load balancer) + HTTPS (ACM cert) trước ECS.
Luồng deploy điển hình (CI/CD)#
- GitHub Actions: lint → test →
docker build→ push image lên ECR. - Đăng ký task definition revision mới trỏ tới image SHA vừa push, rồi update ECS service → rolling deploy (zero-downtime nhờ health check).
- FE riêng: push → Vercel auto deploy.
Dưới đây là job deploy ở dạng rút gọn; job lint/test ở mục 8 chạy trước, nối bằng needs:.
Vì sao đủ bước này mới chạy được:
- Không có
configure-aws-credentialsthì runner chưa có quyền AWS nào,amazon-ecr-loginvàaws ecs ...đều fail. Dùng OIDC (permissions: id-token: write) để job tự xin credentials tạm thời, không lưu access key dài hạn trong GitHub Secrets. --force-new-deploymentmột mình chỉ chạy lại cùng task definition revision (image tag trong đó không đổi), nên không đưa image$GITHUB_SHAlên. Bướcdescribe-task-definition→render→deployđăng ký revision mới có image mới;wait-for-service-stabilitygiữ job đến khi rolling deploy ổn định.- Role
github-deploycần quyền đẩy image lên ECR,ecs:DescribeTaskDefinition,ecs:RegisterTaskDefinition,ecs:UpdateService,ecs:DescribeServicesvàiam:PassRolecho task role / execution role khai báo trong task definition. - Trust policy của role phải khớp claim
subcủa token. Dạng cũ:repo:OWNER/REPO:ref:refs/heads/main. Repo tạo từ 2026-07-15 (hoặc đã opt-in, hoặc đổi tên sau mốc đó) phát claim bất biến kèm ID:repo:OWNER@ORG_ID/REPO@REPO_ID:ref:refs/heads/main. Job chạy trong GitHub environment thìsubchứaenvironment:<tên>thay choref:. Trust policy sai dạng sẽ báoNot authorized to perform sts:AssumeRoleWithWebIdentity(README của action). - Mức đã kiểm: tên input đối chiếu với
action.ymlcủa từng action ở các major trên; workflow này chưa lint lại bằngactionlinttrong lần soát gần nhất (máy không có sẵn) và chưa chạy deploy thật lên AWS.
Mẫu trust policy cho role github-deploy (code tham chiếu, chưa chạy; dạng điều kiện theo README của configure-aws-credentials). Role này cần OIDC provider token.actions.githubusercontent.com đã tạo trong IAM, và Principal là provider đó (Federated), không phải một tài khoản:
aud phải là sts.amazonaws.com (giá trị mặc định của action). StringEquals khớp đúng từng ký tự, nên an toàn; chỉ chuyển sang StringLike với ký tự đại diện (ví dụ repo:OWNER/REPO:*) khi bạn chủ ý cho mọi nhánh, pull request và environment của repo đó assume role, vì như vậy bất kỳ ai đẩy được một nhánh vào repo đều deploy được. Với repo dùng claim bất biến hoặc job có environment, đổi giá trị sub sang dạng tương ứng: repo:OWNER@ORG_ID/REPO@REPO_ID:ref:refs/heads/main hoặc repo:OWNER/REPO:environment:production.
Rollback bằng revision task definition trước#
Mỗi lần deploy ở trên đăng ký một revision mới và giữ revision cũ, nên rollback là trỏ service về revision trước, không cần build lại. Code tham chiếu, chưa chạy (không có tài khoản AWS):
--task-definition nhận family:revision và việc đổi nó tự bắt đầu một deployment, không cần --force-new-deployment (update-service, list-task-definitions, đọc 2026-10-05). Rollback chỉ đưa code về; schema DB đã migrate không tự lùi (xem phần migration bên dưới).
Muốn ECS tự lùi khi deploy hỏng, bật deployment circuit breaker trên service dùng rolling update:
Khi deployment fail, ECS lùi về deployment gần nhất ở trạng thái COMPLETED; nếu chưa có deployment nào COMPLETED thì nó không lùi được và deployment kẹt lại (ECS: deployment circuit breaker, đọc 2026-10-05). Circuit breaker chỉ phát hiện tác vụ không chạy hoặc không qua health check, nên một bug logic vẫn lên prod; health check của mục 12 mới là thứ làm nó có ích.
Chạy migration trước khi đổi service#
Quy tắc: migration là một bước riêng, chạy một lần, và job deploy needs nó. Không để mỗi instance app tự migrate lúc khởi động, vì khi scale ra nhiều instance cùng lúc thì nhiều tiến trình cùng áp một migration.
wait tasks-stoppedhỏi mỗi 6 giây, tối đa 100 lần (khoảng 10 phút), hết lượt thì thoát mã 255; nó chỉ chờ task dừng, không cho biết task thành công hay thất bại, nên phải đọcexitCodeởdescribe-tasks(wait tasks-stopped, run-task, đọc 2026-10-05).- Nếu
run-taskthất bại ngay (thiếu quyền, hết capacity),tasksrỗng vàTASK_ARNlàNone: kiểmfailurestrong output trước khiwait. - Khoá: để hai pipeline không migrate cùng lúc, dùng
concurrencycủa workflow (nhómdeploy-prodở trên,cancel-in-progress: false). Việc Prisma 7 tự chờ nhau qua khoá của Postgres khi chạymigrate deployđồng thời: chưa xác minh trong tài liệu 7.x, nên đừng dựa vào đó. - Tương thích hai chiều: migration chạy trước khi version mới lên, version cũ vẫn đang phục vụ, nên mỗi migration phải chạy được với cả code cũ lẫn code mới (thêm cột nullable trước, bỏ cột ở lần deploy sau); mẫu ở GĐ14 mục 10.
Workflow thật của repo này làm ví dụ#
Repo này tự deploy bằng .github/workflows/deploy.yml, đáng đọc vì nó dùng các ý ở mục 8 trên một việc có thật (build site VitePress rồi đẩy lên Cloudflare Pages):
- Hai job
buildvàdeploy, nối bằngneeds: build;deploychỉ chạy với push lênmainhoặc khi kích hoạt tay (workflow_dispatch) trênmain(ifchặn pull request), nên PR chỉ build. concurrencynhóm theogithub.refvớicancel-in-progress: true: commit mới huỷ lần chạy cũ của cùng nhánh. Hợp với site tĩnh; deploy API ở trên đặtcancel-in-progress: falsevì huỷ giữa chừng một migration là nguy hiểm.- Artifact
sitetruyền kết quả build sang job deploy, nên job deploy không build lại. - Một step kiểm secret Cloudflare và chỉ cảnh báo (không fail) khi thiếu, vì
secretskhông dùng được trongifở cấp job; fork không có secret vẫn build được. - Phiên bản CLI
wranglerđược ghim để khớp với bản dùng khi phát triển local.
Chọn cái nào?#
- Mới học / MVP: FE→Vercel, backend→Railway (hoặc App Runner). Nhanh nhất.
- Production/scale: FE→Vercel, backend→ECS Fargate + RDS(pgvector) + ElastiCache + S3.
Pitfall thực tế#
- Serverless có giới hạn runtime và kết nối; Lambda hỗ trợ response streaming ở một số đường invoke. Kiểm tra giới hạn cụ thể trước khi chọn cho LLM/API dài; worker dài hạn hoặc queue thường dễ vận hành hơn trên ECS/VPS.
- DB connection cạn khi scale nhiều instance → dùng connection pooler.
- Quên set health check → deploy làm rớt request.
Done bổ sung khi#
- Dự án 3 (Mini SaaS API; pgvector chỉ cần khi làm thêm DocuChat ở GĐ22 đến GĐ25): FE chạy Vercel, backend Docker chạy trên AWS ECS Fargate (hoặc Railway), Postgres + Redis managed, deploy qua GitHub Actions.
🔜 Bước tiếp theo — AWS GĐ16, Cloudflare GĐ17, rồi Kubernetes#
GĐ15 dừng ở Docker + PaaS/VPS/ECS. Với một backend + một worker,
docker compose trên VPS hoặc ECS Fargate làm được đúng việc mà Kubernetes làm,
với khoảng 5% độ phức tạp. Tiếp theo là GĐ16 AWS:
hiểu quyền, mạng, database, object storage và cách vận hành một lab nhỏ. Sau đó,
GĐ17 Cloudflare áp dụng backend trên Workers, R2, D1
và Queues để so sánh với runtime AWS.
Vì sao vẫn phải học K8s — và học ở GĐ18:
- Nó có mặt trong mô tả công việc và phỏng vấn ở hầu hết công ty tầm trung trở lên.
- Các khái niệm của nó (declarative, reconciliation loop, liveness vs readiness, resource request/limit, graceful shutdown) là cách tư duy đúng về vận hành, có giá trị kể cả khi bạn chạy trên PaaS.
Học theo thứ tự. Trước K8s, cần Dockerfile multi-stage sạch, CI/CD tự động, ít nhất một lần deploy thủ công thành công ở GĐ15, rồi hoàn thành lab AWS ở GĐ16 và Cloudflare ở GĐ17. K8s không xoá được bài toán nào bạn sắp gặp — app vẫn phải stateless, vẫn cần graceful shutdown, health check và connection pooling (→ GĐ21).
Khi nào K8s thật sự đáng dùng cho dự án của bạn (không phải để học): khi có ít nhất hai trong ba điều sau xảy ra thật — (a) nhiều hơn 4–5 service triển khai độc lập; (b) nhiều môi trường/nhiều vùng cần cấu hình giống hệt nhau; (c) công ty đã dùng K8s và bạn cần làm việc trong đó.
Infrastructure as Code (Terraform/Pulumi) vẫn nằm ngoài lộ trình này. Học nó ngay khi bạn không còn nhớ được tài nguyên đã tạo bằng Console hoặc cần lặp lại môi trường. Thứ tự gợi ý: lab AWS ở GĐ16 → Terraform mô tả VPC/EC2/RDS hiện có → GĐ18 trên cluster local (kind/k3d) → Helm/Kustomize → GitOps (ArgoCD). Đừng bắt đầu bằng cách dựng cluster production.