Skip to content

HARRY-8 — GĐ14 — DevOps & vận hành: Docker, CI/CD, deploy

GĐ14 — 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.


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:

git push → CI chạy test → build Docker image → deploy → health check OK

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.

bash
docker build -t myapi:1.0 .     # tạo image từ Dockerfile
docker run -p 3000:3000 myapi:1.0   # chạy 1 container từ image
docker ps                        # xem container đang chạy

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ải docker 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
# base image nhỏ, pin version cụ thể
FROM node:20-alpine

WORKDIR /app

# copy manifest TRƯỚC để tận dụng cache layer
COPY package.json package-lock.json ./
RUN npm ci                 # cài đúng theo lock, sạch, nhanh, reproducible

# copy source SAU — đổi code không phá cache của npm ci
COPY . .

EXPOSE 3000                # tài liệu hóa port (không tự publish)
CMD ["node", "dist/server.js"]   # lệnh chạy khi container start

Giải thích từng phần.

  • node:20-alpine: Alpine là distro Linux siêu nhẹ (~5MB base) → image nhỏ. Pin 20 thay vì latest để build reproducible.
  • npm ci thay vì npm install: ci cài chính xác theo package-lock.json, xóa node_modules cũ 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:3000 khi docker run.
  • CMD dạng exec (["node", ...]) tốt hơn dạng shell (node ...): process Node là PID 1, nhận signal SIGTERM đúng cách → graceful shutdown.

Pitfall / case thực tế.

  • COPY . . TRƯỚC npm ci → mỗi lần đổi 1 dòng code, cache npm ci vỡ, cài lại toàn bộ deps. Đây là lỗi #1 làm CI chậm.
  • Alpine dùng musl libc thay vì glibc → vài native module (bcrypt, sharp, node-gyp) build lỗi. Khi đó dùng node:20-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.

dockerfile
# ---- stage 1: build ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci                     # cả devDependencies (tsc, types...)
COPY . .
RUN npm run build              # tsc → dist/

# ---- stage 2: runtime ----
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev          # CHỈ prod deps
COPY --from=builder /app/dist ./dist   # chỉ lấy output
EXPOSE 3000
CMD ["node", "dist/server.js"]

Pitfall / case thực tế.

  • Quên --omit=dev ở stage runtime → vẫn ship typescript, @types/*, jest... image phình to.
  • Copy nhầm node_modules từ builder (có cả devDeps) sang runtime → mất tác dụng multi-stage. Nên copy dist/ và cài lại prod deps riêng.
  • Native module cần compile: nếu build ở builder rồi copy node_modules sang runtime, phải chắc hai stage cùng OS/arch, nếu không binary không tương thích.

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.

node_modules
npm-debug.log
.git
.env
.env.*
dist
coverage
Dockerfile
.dockerignore

Pitfall / case thực tế.

  • Không có .dockerignore → copy node_modules local (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, ai docker 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ải localhost.
  • 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.

yaml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@postgres:5432/app
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy   # chờ DB healthy, không chỉ started
    develop:
      watch: [] # optional

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data   # data bền
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine

volumes:
  pgdata:

Pitfall / case thực tế.

  • App connect localhost:5432 thay vì postgres:5432 → fail. Trong compose network, localhostchính container đó, không phải host. Phải dùng tên service.
  • depends_on KHÔ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ùng condition: service_healthy + healthcheck, hoặc retry logic trong app.
  • Quên volume cho Postgres → docker compose down là 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 .env local (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.

ts
// config.ts — validate & fail-fast lúc boot
import { z } from "zod";
const env = z.object({
  NODE_ENV: z.enum(["development", "staging", "production"]),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
}).parse(process.env);   // thiếu/sai → crash ngay, không chạy nửa vời
export default env;
# .env.example (COMMIT cái này, không giá trị thật)
DATABASE_URL=postgres://user:pass@localhost:5432/app
JWT_SECRET=change-me-32-chars-minimum-xxxxxxxx

Pitfall / case thực tế.

  • Commit .env thậ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_URL undefined. 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.

yaml
name: CI
on:
  push: { branches: [main] }
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm            # cache ~/.npm giữa các run
      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build

  deploy:
    needs: test                # chỉ deploy khi test pass
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "deploy step (railway up / docker push / ssh...)"

Pitfall / case thực tế.

  • Không cache deps → mỗi run npm ci tải lại từ đầu, pipeline chậm gấp đôi. cache: npm hoặc actions/cache khắ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).

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.

nginx
upstream app {
    server app1:3000;
    server app2:3000;        # load balance 2 instance
}
server {
    listen 443 ssl;
    server_name api.example.com;
    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    location / {
        proxy_pass http://app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

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.

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), cert hạn 90 ngày, tự động renew qua cron.
  • 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.

bash
# tự quản VPS: xin + tự renew cert cho Nginx
sudo certbot --nginx -d api.example.com
# certbot tự thêm dòng ssl_certificate vào Nginx và cài cron renew

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→https vô hạn. Đọc X-Forwarded-Proto thay vì tự đoán.
  • Với platform managed thì đừng tự cài Certbot — sẽ xung đột với TLS của platform.

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óngVài phút, git push là chạyChậm, phải setup server
TLS / proxy / scalingTự độngTự 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 scaleRẻ hơn khi tải lớn, cố định
Học được gìÍt về opsHiểu sâu Linux/network/Docker
DebugLog qua dashboardSSH vào máy, full access

Ví dụ ngắn.

bash
# PaaS: deploy trong 1 lệnh
railway up
# VPS: build + chạy bằng compose trên server
ssh deploy@server 'cd /app && git pull && docker compose up -d --build'

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: endpoint (/health) để platform/proxy biết instance còn sống & sẵn sàng.
  • 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: trả 200 khi app + dependency (DB) OK; trả 503 khi chưa sẵn sàng → LB không route tới.
  • Graceful shutdown: nhận SIGTERM → ngừng nhận request mới, xử nốt request đang chạy, đóng DB pool, rồi exit.
  • 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.

ts
// health endpoint
app.get("/health", async (_req, res) => {
  try { await db.raw("select 1"); res.status(200).json({ status: "ok" }); }
  catch { res.status(503).json({ status: "degraded" }); }
});

// graceful shutdown
process.on("SIGTERM", async () => {
  server.close(() => {});        // ngừng nhận request mới
  await db.destroy();            // đóng pool
  process.exit(0);
});
bash
docker logs -f --tail 100 <container>   # xem log realtime
docker compose logs -f app

Pitfall / case thực tế.

  • Health check chỉ trả 200 OK cứ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. Health nên phản ánh dependency thật (nhưng đừng để check quá nặng gây flapping).
  • Không xử SIGTERM → deploy làm rớt request đang chạy (mất data/transaction dở). Luôn graceful shutdown.
  • 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):

  1. Viết Dockerfile multi-stage cho service Node (stage build → stage runtime, npm ci --omit=dev).
  2. Thêm .dockerignore (node_modules, .git, .env, dist).
  3. docker-compose.yml: services app + postgres + redis, network mặc định, volume pgdata, depends_on với condition: service_healthy, healthcheck cho Postgres.
  4. Config qua env: config.ts validate bằng zod; .env (gitignored) + .env.example (commit).
  5. docker compose up --build → app connect DB qua hostname postgres, không phải localhost.

CI/CD (GitHub Actions):

  1. Workflow on: [push, pull_request]: checkout → setup-node (cache npm) → npm ci → lint → test → build.
  2. Job deploy với needs: testif: main → push image / railway up / ssh + docker compose up -d --build.
  3. Secrets qua ${{ secrets.* }}, không hardcode, không echo.

Prod:

  1. Deploy PaaS trước (Railway/Render) cho nhanh; TLS + proxy platform lo.
  2. Thêm endpoint /health (check DB) + graceful shutdown (SIGTERM).
  3. Log structured ra stdout; xem qua dashboard.

Done khi

  • [ ] Giải thích được image vs container, container vs VM bằng lời của mình.
  • [ ] Viết được Dockerfile multi-stage cho Node đúng thứ tự cache (COPY package.json trước, npm ci, rồi COPY source).
  • [ ] Có .dockerignore loại node_modules/.env/.git.
  • [ ] docker compose up dựng được app + Postgres + Redis, app connect DB qua tên service, data bền qua volume.
  • [ ] Config toàn bộ qua env (validate fail-fast), .env gitignored, chỉ commit .env.example.
  • [ ] Có workflow GitHub Actions: lint → test → build, cache deps, deploy chỉ khi test pass và đúng branch.
  • [ ] 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.
  • [ ] Có /health phản ánh dependency, có graceful shutdown, biết xem log prod.
  • [ ] Deploy thật Dự án 3 lên một môi trường và truy cập được qua HTTPS.

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).
  • 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)?
  • Observability sâu hơn: metrics (Prometheus) + tracing (OpenTelemetry) khác gì với logging thuầ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 (10–60s tuỳ plan) và không giữ stateKHÔNG hợp cho LLM streaming dài, 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 (LLM streaming, 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

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, KHÔNG hợp streaming LLM dài.

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)

  1. GitHub Actions: lint → test → docker build → push image lên ECR.
  2. Update ECS service → rolling deploy (zero-downtime nhờ health check).
  3. FE riêng: push → Vercel auto deploy.
yaml
# .github/workflows: bước build & push image len ECR (rut gon)
- uses: aws-actions/amazon-ecr-login@v2
- run: |
    docker build -t $ECR/app:$GITHUB_SHA .
    docker push $ECR/app:$GITHUB_SHA
- run: aws ecs update-service --cluster prod --service app --force-new-deployment

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 (Vercel/Lambda) + LLM streaming/queue = sai kiến trúc → luôn tách backend stateful ra 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 (AI SaaS): FE chạy Vercel, backend Docker chạy trên AWS ECS Fargate (hoặc Railway), Postgres+pgvector + Redis managed, deploy qua GitHub Actions.

🔜 Bước tiếp theo — Kubernetes & Infrastructure as Code

Giai đoạn này dừng ở Docker + PaaS/VPS/ECS, và đó là điểm dừng đúng cho DA3. 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. Đừng vội.

Vì sao vẫn phải học K8s — và học ở GĐ15:

  • 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.

Nhưng học đúng thứ tự. Ba thứ ở giai đoạn này là điều kiện tiên quyết thật sự: Dockerfile multi-stage sạch, CI/CD tự động, và ít nhất một lần deploy thủ công thành công. 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, vẫn cần health check, vẫn cần connection pooling (→ GĐ18).

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. Nó đáng học sớm hơn K8s một chút — ngay khi hạ tầng cloud vượt quá mức "bấm tay trong console mà vẫn nhớ được". Thứ tự gợi ý: Terraform mô tả hạ tầng ECS/RDS hiện có → GĐ15 trên cluster local (kind/k3d) → Helm/Kustomize → GitOps (ArgoCD). Đừng bắt đầu bằng cách dựng cluster production.

Học bằng cách build. Chứng minh, đừng tin.